Addition.js 17 KB

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