Addition.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. import React, { useState, useEffect, useRef } from "react";
  2. import DataGrid, {Column} from "devextreme-react/data-grid";
  3. import {numbersToArrOfArr, afterCommaLen, handleKeyDown} from "./helpers.js";
  4. import "./App.css";
  5. let imdtRes;
  6. let imdtResIdx;
  7. let resArr = [];
  8. let noOfDigits = 0;
  9. let commaIsSet = false;
  10. function Addition() {
  11. const [input, setInput] = useState(""); // initial user input like 34+56.7
  12. const [realResult, setRealResult] = useState(0); // real result of calculation
  13. const [carryArr, setCarryArr] = useState([]); // array of carries ["1","0"]
  14. const [commaIdx, setCommaIdx] = useState(-1); // int: comma position
  15. const [numbers, setNumbers] = useState(0); // array of arrays with input numbers
  16. const [resultsGrid, setResultsGrid] = useState([{id: 1, number: ""}]); // calculated result
  17. const [stepsGrid, setStepsGrid] = useState([{id: 1, step: ""}]); // one step for every digit
  18. const [autoMoveComma, setAutoMoveComma] = useState(true); // move the comma automatically
  19. const [allowStartOver, setAllowStartOver] = useState(true); // show steps and allow restart
  20. // focus the input field
  21. let calculationInput = useRef(null);
  22. useEffect(()=>{
  23. if(calculationInput.current && calculationInput.current.value === ""){
  24. calculationInput.current.focus();
  25. }
  26. }, []);
  27. useEffect(()=>{
  28. if(!autoMoveComma){
  29. addNumbersToTable([]);
  30. }
  31. }, [numbers]);
  32. const handleInput = (e) => {
  33. setInput(e.target.value);
  34. };
  35. const handleResChange = (e) => {
  36. if(typeof e === "string"){
  37. imdtRes = e;
  38. }else{
  39. imdtRes = e.target.value;
  40. }
  41. // remove deleted input
  42. if(imdtRes === ""){
  43. resArr.shift();
  44. }else{
  45. // add input number to result array
  46. resArr.unshift(imdtRes);
  47. }
  48. };
  49. const handleCarryChange = (e, nosLeft) => {
  50. if(e.keyCode === 13){
  51. let carryArrCopy = [...carryArr];
  52. carryArrCopy.unshift(e.target.value);
  53. setCarryArr(carryArrCopy);
  54. let noCarry = carryArrCopy[0] === "" || carryArrCopy[0] === "0" || carryArrCopy[0] === undefined;
  55. if((nosLeft === 1 && noCarry) ||
  56. // if stop after first iteration, numbers left is undefined at first
  57. (isNaN(nosLeft) && realResult.toString().length === 1 && noCarry)){
  58. nosLeft = 0;
  59. handleResChange(" ");
  60. }
  61. if(nosLeft === 0){
  62. if(allowStartOver){
  63. showIdmtResults(carryArrCopy);
  64. addButtonsToImdtSteps(carryArrCopy);
  65. }else{
  66. finishCalculation();
  67. }
  68. }
  69. }
  70. };
  71. const handleSubmit = (e) => {
  72. startCalculation();
  73. e.preventDefault(); // avoid page reload
  74. };
  75. const startCalculation = () => {
  76. if(input.includes("+")){
  77. let numbers = input.split("+").map(x => parseFloat(x.replace(",",".")));
  78. let realRes = numbers.reduce((x,y) => x+y, 0);
  79. let afterComma = Math.max(...numbers.map(x => afterCommaLen(x)));
  80. realRes = parseFloat(realRes.toFixed(afterComma));
  81. setRealResult(realRes);
  82. console.log("real result:", realRes);
  83. let numbersArr, commaIdx;
  84. if(!autoMoveComma){
  85. numbersArr = numbers.map(x => x.toString().split(""));
  86. setNumbers(numbersArr);
  87. addNumbersToTable([]);
  88. startCommaMove();
  89. }else{
  90. commaIsSet = true;
  91. [numbersArr, commaIdx] = numbersToArrOfArr(numbers);
  92. setNumbers(numbersArr);
  93. setCommaIdx(commaIdx);
  94. addNumbersToTable(numbersArr);
  95. }
  96. }
  97. };
  98. const handleKeyMoveComma = (e) => {
  99. let pressedKey = e.keyCode;
  100. let tdId = e.target.id;
  101. let noId = tdId.split("numbersTd")[1];
  102. let numbersCopy = [...numbers];
  103. if(pressedKey === 37){ // left arrow
  104. numbersCopy[noId].push(" ");
  105. setNumbers(numbersCopy);
  106. setTimeout(() => {
  107. document.getElementById(tdId).focus();
  108. }, 100);
  109. }else if(pressedKey === 39 && // right arrow
  110. numbersCopy[noId][numbersCopy[noId].length-1] === " "){
  111. numbersCopy[noId].pop();
  112. setNumbers(numbersCopy);
  113. setTimeout(() => {
  114. document.getElementById(tdId).focus();
  115. }, 100);
  116. }else if(pressedKey === 38){ // up
  117. let nextFocusId = Math.max(noId-1, 0);
  118. document.getElementById("numbersTd" + nextFocusId).focus();
  119. }else if(pressedKey === 40){ // down
  120. let nextFocusId = Math.min(noId+1, numbers.length-1);
  121. document.getElementById("numbersTd" + nextFocusId).focus();
  122. }
  123. };
  124. const addNumbersToTable = (numbersArr) => {
  125. let nos;
  126. if(numbersArr.length > 0){
  127. nos = numbersArr;
  128. }else{
  129. nos = numbers;
  130. }
  131. let table = document.getElementById("numbersGrid");
  132. table.innerHTML = "";
  133. for(let noIdx in nos){
  134. let row = table.insertRow();
  135. let cell = row.insertCell();
  136. let cellText = nos[noIdx].join("");
  137. if(noIdx == nos.length-1){
  138. cellText = "+ " + cellText;
  139. }
  140. cell.innerHTML = cellText;
  141. cell.tabIndex = "0";
  142. cell.id = "numbersTd" + noIdx;
  143. }
  144. };
  145. const ResultCarryForm = ({handleResChange, handleCarryChange}) => {
  146. let resInputField = useRef(null);
  147. let carryInputField = useRef(null);
  148. let labelText = "";
  149. let nosLeft = imdtResIdx + 1;
  150. let carryText = "Übertrag = ";
  151. if(commaIsSet && typeof numbers === "object" && imdtResIdx !== -1){
  152. document.getElementById("commaWarning").innerHTML = "";
  153. // set the digit index, start with the last digit
  154. if(typeof imdtResIdx === "undefined" || imdtResIdx === null){
  155. noOfDigits = Math.min(...numbers.map(n => n.length));
  156. imdtResIdx = noOfDigits - 1;
  157. }
  158. if(typeof nosLeft === "undefined" ){
  159. nosLeft = imdtResIdx + 1;
  160. }
  161. // skip over comma
  162. if(imdtResIdx === commaIdx){
  163. if(!resArr.includes(".")){
  164. handleResChange(".");
  165. }
  166. imdtResIdx = imdtResIdx - 1;
  167. nosLeft = nosLeft - 1;
  168. }
  169. // iterate numbers for this digit index
  170. for(let n in numbers){
  171. // skip last empty number if no carry
  172. if(nosLeft === 1 && (carryArr[0] === "0" || carryArr[0] === "")){
  173. nosLeft = 0;
  174. imdtResIdx = -1;
  175. return <></>;
  176. }
  177. // create label text like: d + d + .. = input
  178. let digit = numbers[parseInt(n)][imdtResIdx];
  179. if(digit === "" || digit === "&nbsp;"){
  180. digit = 0;
  181. }
  182. labelText += digit;
  183. if(parseInt(n) === numbers.length - 1){
  184. if(carryArr[0] !== undefined && carryArr[0] > 0){
  185. labelText += " + " + carryArr[0].toString();
  186. }
  187. labelText += " = ";
  188. }else{
  189. labelText += " + ";
  190. }
  191. }
  192. imdtResIdx -= 1;
  193. nosLeft -= 1;
  194. return (
  195. <form>
  196. <label htmlFor="input_result" id="input_result_label">
  197. {labelText}
  198. </label>
  199. <input
  200. onKeyDown={(e) => handleKeyDown(e)}
  201. onChange={(e) => handleResChange(e)}
  202. type="text" id="input_result" size="2" maxLength="1"
  203. aria-labelledby="input_result_label" aria-required="true"
  204. ref={resInputField} autoFocus/>
  205. <br/>
  206. <label htmlFor="input_carry" id="input_carry_label">
  207. {carryText}
  208. </label>
  209. <input
  210. onKeyDown={(e) => handleCarryChange(e, nosLeft)}
  211. type="text" id="input_carry" size="2" maxLength="1"
  212. aria-labelledby="input_carry_label" aria-required="true"
  213. ref={carryInputField}/>
  214. </form>
  215. );
  216. }else{
  217. return (
  218. <>
  219. </>
  220. );
  221. }
  222. };
  223. const addButtonsToImdtSteps = (carryArrCopy) => {
  224. // workaround to access DOM td after grid values are set
  225. setTimeout(() => {
  226. let table = document.getElementById("idmtResultSteps").getElementsByTagName("table")[0];
  227. let trs = table.getElementsByTagName("tr");
  228. for(let trIdx in trs){
  229. let tr = trs[trIdx];
  230. if(tr instanceof HTMLElement && trIdx < trs.length-1){
  231. let td = tr.getElementsByTagName("td")[0];
  232. let btn = document.createElement("button");
  233. btn.innerHTML = "hier starten";
  234. btn.classList = "btn btn-secondary btn-sm";
  235. btn.id = "btn-" + trIdx;
  236. btn.addEventListener("click", () => startOver(trIdx, carryArrCopy));
  237. td.appendChild(btn);
  238. }
  239. }
  240. }, 300);
  241. };
  242. const showIdmtResults = (carryArrCopy) => {
  243. let realResArr = realResult.toString().split("");
  244. while(realResArr.length < resArr.length){
  245. realResArr.unshift("&nbsp;"); // add " " before
  246. }
  247. let carries = carryArrCopy.map(x => x || "0");
  248. carries.push("0");
  249. while(carries.length < resArr.length){
  250. carries.unshift("0"); // add "0" before
  251. }
  252. let foundComma = false;
  253. let stepsGridCopy = [];
  254. for(let i=0; i<resArr.length; i++) {
  255. let text = "";
  256. let trueNumbers = false;
  257. let idxNumbers = resArr.length - i - 1;
  258. let idxCarry = carries.length - i - 1;
  259. let realSum = 0;
  260. for(let j=0; j<numbers.length; j++){
  261. let no = numbers[j][idxNumbers];
  262. if(no!=="." && no!=="&nbsp;"){
  263. trueNumbers = true;
  264. realSum += parseInt(no);
  265. }
  266. text += no;
  267. if(j<numbers.length-1){
  268. text += " + ";
  269. }
  270. }
  271. // ignore indexes without real digits
  272. if(resArr[idxNumbers]!=="&nbsp;" || (carries[idxCarry]!=="&nbsp;" & carries[idxCarry]!=="0")){
  273. trueNumbers = true;
  274. }
  275. // carry array has no "."
  276. if(resArr[idxNumbers] === "."){
  277. foundComma = true;
  278. trueNumbers = false;
  279. }
  280. if(foundComma){
  281. idxCarry += 1;
  282. }
  283. // add carry only if > 0
  284. if(carries[idxCarry]!=="0"){
  285. realSum += parseInt(carries[idxCarry]);
  286. text += " + " + carries[idxCarry]; // + " Übertrag"
  287. }
  288. let realCarry = parseInt(realSum/10).toString();
  289. realSum = (realSum % 10).toString();
  290. text += " = ";
  291. if(trueNumbers){
  292. text += resArr[idxNumbers];
  293. text += " mit Übertrag " + carries[idxCarry-1];
  294. text = text.replace(/&nbsp;/g, "0");
  295. text += resArr[idxNumbers]===realSum && carries[idxCarry-1]===realCarry ? ": Richtig " : ": Falsch ";
  296. stepsGridCopy.push({step: text});
  297. }
  298. }
  299. let paragraph = document.getElementById("stepsParagraph");
  300. paragraph.innerHTML = "Rechenschritte: ";
  301. let btnSubmit = document.createElement("button");
  302. btnSubmit.innerHTML = "Ergebnis abgeben";
  303. btnSubmit.addEventListener("click", finishCalculation);
  304. btnSubmit.classList = "btn btn-secondary btn-sm";
  305. btnSubmit.id = "btnSubmitSteps1";
  306. document.getElementById("idmtResultSteps").insertBefore(btnSubmit, paragraph);
  307. setStepsGrid(stepsGridCopy);
  308. let btnSubmit2 = document.createElement("button");
  309. btnSubmit2.innerHTML = "Ergebnis abgeben";
  310. btnSubmit2.addEventListener("click", finishCalculation);
  311. btnSubmit2.classList = "btn btn-secondary btn-sm";
  312. btnSubmit2.id = "btnSubmitSteps2";
  313. document.getElementById("idmtResultSteps").appendChild(btnSubmit2);
  314. document.getElementById("idmtResultSteps").style.display = "inline-block";
  315. document.getElementById("idmtResultSteps").tabIndex = 0;
  316. document.getElementById("idmtResultSteps").focus();
  317. };
  318. const startOver = (idx, carryArrCopy) => {
  319. let invertedIdx = numbers[0].length - idx - 1;
  320. imdtResIdx = invertedIdx;
  321. let slicer = invertedIdx;
  322. if(resArr[0] !== "&nbsp;" && !resArr.includes(".")){
  323. slicer += 1;
  324. }else if(resArr[0] === "&nbsp;" && resArr.includes(".")){
  325. slicer -= 1;
  326. }
  327. carryArrCopy = carryArrCopy.slice(slicer);
  328. // reset result and carries until the chosen step
  329. let commaPos = resArr.indexOf(".");
  330. if(resArr.includes(".") && commaPos >= invertedIdx){
  331. imdtResIdx -= 1;
  332. resArr = resArr.slice(invertedIdx);
  333. }else{
  334. resArr = resArr.slice(invertedIdx + 1);
  335. }
  336. setCarryArr(carryArrCopy);
  337. document.getElementById("btnSubmitSteps1").remove();
  338. document.getElementById("btnSubmitSteps2").remove();
  339. document.getElementById("idmtResultSteps").style.display = "none";
  340. };
  341. const finishCalculation = () => {
  342. document.getElementById("idmtResultSteps").innerHTML = "";
  343. let resCalc = resArr.filter(n => n !== "&nbsp;").join("");
  344. setResultsGrid([{number: resCalc}]);
  345. resCalc = parseFloat(resCalc);
  346. let message = "";
  347. if(resCalc === realResult){
  348. message = "Richtig!";
  349. }else{
  350. message = "Das ist leider falsch.";
  351. }
  352. message = "<p>" + message + "</p>";
  353. document.getElementById("finishCalculation").innerHTML = message;
  354. document.getElementById("finishCalculation").tabIndex = "0";
  355. document.getElementById("finishCalculation").focus();
  356. };
  357. const startCommaMove = () => {
  358. document.getElementById("commaSubmit").style.display = "inline-block";
  359. setTimeout(() => {
  360. document.getElementById("numbersTd0").focus();
  361. }, 300);
  362. };
  363. const submitCommaMove = () => {
  364. let numbersCopy = [...numbers];
  365. let commaPositions = [];
  366. // reset numbers to calculate new length after error
  367. for(let noIdx in numbersCopy){
  368. while(numbersCopy[noIdx][0] === "&nbsp;"){
  369. numbersCopy[noIdx].shift();
  370. }
  371. }
  372. // get all numbers up to the same length
  373. let numbersLen = Math.max(...numbers.map(n => n.length));
  374. for(let noIdx in numbersCopy){
  375. while(numbersCopy[noIdx].length <= numbersLen){
  376. numbersCopy[noIdx].unshift("&nbsp;");
  377. }
  378. commaPositions.push(numbersCopy[noIdx].indexOf("."));
  379. }
  380. setNumbers(numbersCopy);
  381. let commaCorrect = false;
  382. commaPositions = [...new Set(commaPositions.filter(c => c >= 0))];
  383. // if no commas in numbers
  384. if(commaPositions.length === 0){
  385. setCommaIdx(-1);
  386. // check if no number was moved
  387. commaCorrect = !numbersCopy.map(n => n[numbersLen] !== "&nbsp;").includes(false);
  388. // correct comma positions
  389. }else if(commaPositions.length === 1){
  390. setCommaIdx(commaPositions[0]);
  391. commaCorrect = true;
  392. // check if numbers without comma are correct too
  393. for(let no of numbersCopy){
  394. if(!no.includes(".")){
  395. // digit before comma is a number, no space
  396. if(no[commaPositions[0]-1] === "&nbsp;"){
  397. commaCorrect = false;
  398. }
  399. // only spaces from comma to end
  400. for(let digit=commaPositions[0]; digit<=numbersLen; digit++){
  401. if(no[digit] !== "&nbsp;"){
  402. commaCorrect = false;
  403. }
  404. }
  405. }
  406. }
  407. }
  408. if(commaCorrect){
  409. document.getElementById("commaSubmit").style.display = "none";
  410. commaIsSet = true;
  411. }else{
  412. let paragraph = document.getElementById("commaWarning");
  413. paragraph.innerHTML = "das ist leider falsch, versuche es noch einmal:";
  414. paragraph.tabIndex = "0";
  415. paragraph.focus();
  416. }
  417. };
  418. return (
  419. <main>
  420. <h1>Addition</h1>
  421. <form onSubmit={(e) => handleSubmit(e)}>
  422. <label htmlFor="calculationInput">
  423. Rechnung:
  424. </label>
  425. <input
  426. type="text"
  427. id="calculationInput"
  428. onChange={(e) => handleInput(e)}
  429. aria-label="Rechnung"
  430. aria-required="true"
  431. ref={calculationInput}/>
  432. <input type="submit" value="berechnen"/>
  433. </form>
  434. <div id="finishCalculation"></div>
  435. <div id="commaWarning"></div>
  436. <div id="overview">
  437. <table
  438. id="numbersGrid"
  439. onKeyDown={handleKeyMoveComma}
  440. >
  441. </table>
  442. <input type="submit"
  443. value="Komma best&auml;tigen"
  444. id="commaSubmit"
  445. onClick={() => submitCommaMove()}/>
  446. <hr></hr>
  447. <DataGrid
  448. dataSource={resultsGrid}
  449. focusedRowEnabled={true}
  450. showBorders={true}
  451. showColumnHeaders={false}
  452. >
  453. <Column dataField="number" />
  454. </DataGrid>
  455. </div>
  456. <div id="calculation">
  457. <ResultCarryForm
  458. handleResChange={handleResChange}
  459. handleCarryChange={handleCarryChange}/>
  460. </div>
  461. <div id="idmtResultSteps">
  462. <p id="stepsParagraph"></p>
  463. <DataGrid
  464. dataSource={stepsGrid}
  465. focusedRowEnabled={true}
  466. showBorders={true}
  467. showColumnHeaders={false}
  468. noDataText=""
  469. >
  470. <Column dataField="step" />
  471. </DataGrid>
  472. </div>
  473. </main>
  474. );
  475. }
  476. export default Addition;