Subtraction.js 18 KB

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