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