calc82: Web Scientific Calculator
- 14 Devlogs
- 120 Total hours
A calculator web-app inspired by the Casio fx-82 series of scientific calculators.
A calculator web-app inspired by the Casio fx-82 series of scientific calculators.
calc82: because calculators are for everyone
Final devlog. It’s done!! This last stretch was writing the readme and publishing everything to Github pages. You can now access it at https://rigelseven.github.io/calc82/
calc82 is the largest personal software project I have ever completed. This project taught me a lot about input parsing - specifically, tokenisers and syntax trees - and the importance of keeping a large codebase tidy.
Overall, the structure of the project is well laid out. However, there are architectural decisions I made early on that I came to regret later (such as the fact that input is represented as a string, whereas it should probably be a tree).
Developing calc82 also showed me that I am capable of putting together such an app from scratch. I’m pleased to have participated in Stardance and be given this opportunity.
calc82: because calculators are for everyone
This devlog follows the addition of help and about menus to my calculator, as well as bundling into one js file.
I chose to use esbuild for bundling. This allows the many JS files to be condensed down into one file, reducing the number of requests that need to be made. This build system is much simpler than other options such as node.js.
I also added a simple service worker that caches all requests locally. This makes the calculator available offline.
I chose GPLv3 for this project as it is an end-user product. Essentially, this means others are free to modify and distribute the project as long as they make the source available. I also added legal disclaimers to the about page of the calculator.
Write the readme, publicise the repo, and host!
Thanks for reading :)
calc82: because calculators are for everyone
This devlog follows the addition of customisable and accessible keyboard bindings to my calculator.
The default keyboard map has been implemented as a const dictionary for a while now, allowing lookup of buttons from keyboard keys. This was extended with a user dictionary that can be dynamically updated. However, generating the help hotkey hints needed to go in the opposite direction of the dictionary.
The naive approach of iterating through values to find a key for each would perform poorly with O(n^2). Instead, we precalculate a reversed dictionary to lookup keyboard keys from each button. This precalculation is O(n), and setting the keys is also O(n), resulting in O(n) time overall.
Development
Preparation for shipping
Thanks for reading :)
calc82: because calculators are for everyone
This devlog follows the addition of a few final features to my calculator, and then my worst nightmare, frontend UI 😧
Prime factorisation or integer factorisation is the decomposition of a positive integer into a product of integers.
The simplest way to implement this is trial division, which is straightforward but has O(√n) time complexity. I thought this would be too slow, so I decided to implement Pollard’s rho algorithm. This has a nicer complexity of O(√p) where p is the smallest prime factor of the number.
However, further observation showed that that the Casio calculator probably doesn’t do this - for numbers without easy factors, it just doesn’t bother. Despite this, I’ve already implemented the algorithm, so I’m not going to remove functionality for the sake of exactly replicating the calculator’s behaviour.
My goal with the styling was to make the calculator as similar across browsers as possible. I bundled the Inter font (best font) into the program and removed browser-specific button styling.
Implementing the CSS styling was mostly painless. I opted for a simple design with a menubar that adapts to horizontal or vertical depending on the window’s aspect ratio for the web app functionality. Additionally the css uses the system theme to automatically choose light or dark mode.
This project is nearly finished - should only be one or two devlogs left until I ship. Here’s what I have left to do:
Development
Preparation for shipping
Webpack or similar
Choose a license and publicise repo
Write a readme
Host on github pages (?)
Hope that I did enough to not receive legal action from casio 🙃
Thanks for reading :)
calc82: because calculators are for everyone
This devlog follows the addition of a lot of small features to my calculator that were last on my to-do list, as well as a slight shift in scope.
This is an operator I’ve never used on my calculator, but its quite powerful. you can type Ans+1:Ans*2:Ans^2 and it will calculate the first expression, then the second, and so on until it loops back to the start.
Although this functionality is not part of the original calculator, it makes it much more pleasant to use on a computer keyboard. This was implemented as a hidden button.
Scrolling was surprisingly easy to implement! The browser handles all of the hard work, and all I had to do was set the right CSS tags. Granted, with my CSS experience, this was still difficult, but I made it work in the end.
I have decided to shift the current scope of the project (before shipping) to just be implementing the main compute mode. This means I will not need to do STAT, VERIFY or LINEIO mode. This is for three reasons:
With this shift in scope, the calculator part of the project is all but done - I only have three buttons remaining. (I had to push this devlog out before 10 hours, which is why they’re not done). Then, I can focus on adding the surrounding UI and UX, which should make it actually possible for other people to use my project.
Thanks for reading :)
calc82: because calculators are for everyone
This devlog follows the addition of menus and the status bar to my calculator.
The menu system uses a dictionary to encode the menu pages and their entries, making it easy to add new menus. However, I couldn’t find a way to easily encode functions and arguments in this structure, so the actual actions are handled by a big switch statement. When the menu is active, most buttons have no action and the input/output display is hidden.
The status bar consists of a row of elements with adjustable opacity (to represent their active state). It is handled by a class which allows items to be toggled with a single method call, meaning it can retroactively be integrated into other components.
The original calculator has some menu elements that can never be activated - for example, there is a complex mode indicator, but the calculator cannot show complex numbers. I omitted these on calc82, as they took up unnecessary space.
The project is almost nearing completion :) I made complete list of every feature missing from the calculator (too long to post here). It should should get shorter every devlog from now! Hopefully I can finally ship this project in the next couple of weeks.
Thanks for reading :)
calc82: because calculators are for everyone
Sorry its been a while since the last devlog, I’ve had this one sitting for a while and not had the chance to finish and post it.
This devlog follows the improvement of the user interface to my calculator, as well as the addition of variables.
It was difficult porting the modifier (shift/alpha) keys to keyboard because Shift is used to type symbols such as + and *, and there is also a Shift key on the calculator…
My solution was making tapping shift without any other input to toggle the calculator shift modifier, and combining it with another key to not toggle it.
Use of the calculator
I’ve actually started using the calculator in class, and it’s been faithful to the original so far (for implemented functionality). I still don’t really trust it though and want to run it through some formalised tests before shipping.
Keyboard mappings
So far, I have been hardcoding keyboard mappings based on what feels natural to me. However, it would probably be better to have user-editable mappings for other people to customise.
Immediate:
Medium:
Long-term:
calc82: because calculators are for everyone
This devlog follows the introduction of an actual user interface to my calculator.
The calculator needed buttons resembling that of the Casio to fulfil my purpose.
Buttons are dynamically built from an array, which defines their labels and actions. This allows easy changes.
For most buttons, the area surrounding the button also acts as a clickable area for better touch input. However, I didn’t find an easy way to do this for the navigation buttons (which are positioned off the grid).
CSS Grid allows a 2D layout system based on rows and columns. It has been supported by all major browsers since October 2017.
CSS Grid was used, as it allows consistent positioning across browsers. In addition, relative units were used to prevent resizing the calculator from affecting it. This should make it easier to make dynamic sizing in the future.
Accessibility
<button>s are preferred over clickable <div>s because they work with screen readers and legacy browsers. However, I also made the surrounding elements clickable for a better touch experience. Also, keyboard input will make use significantly easier for computer users.
Aesthetics
The CSS applied so far is purely functional. In the future, I will improve the aesthetics of the calculator through CSS - but for now, this barebones look makes development easier and more focused.
The todo list was getting too empty, so it has been updated with new tasks
Immediate:
Medium:
Long-term:
calc82: because calculators are for everyone
This devlog follows the further development of algebraic I/O to my calculator, including turning this into an actual result.
inputHandler -> [input tokens] -> tokeniser -> [calculator tokens] -> parser -> AST -> evaluator -> result
This:
sin(), and calculator tokens are whole numbers and separated symbols.Avoiding the lazy route
My first approach was turning the input tokens into a string, which my tokeniser already knew how to deal with. However, this added much processing overhead and made it harder to find out the position at which errors occurred. Spending the time to implement the token conversion made this step much easier
Misplaced math errors
When implementing error handling, I passed down the original position of each token in the input token array down to each step of the calculation process. However, because my evaluator is recursive, it would constantly update the error position to the latest token, meaning the errors were always placed at the end. This was an easy fix in the end - but hard to spot!
Immediate:
Medium:
Long-term (but getting closer!):
calc82: because calculators are for everyone
This devlog mainly follows the addition of algebraic I/O to my calculator. This is just a check-in since I was getting close to 10 hours since the last devlog - I still have some work to do on this aspect before I move on.
I decided on KaTeX because it is both fast (no reflow!) and does not rely on dependencies.
KaTeX is a math formatting library that uses LaTeX commands to create beautiful equations from code.
For example,
\frac{2^{\frac{3}{4}}}{4\sin(30)}\times 2\pi
turns into the attached image.
Input is currently captured directly from the keyboard to build a 1D array of tokens. The tokens are then converted into KaTeX formatting for algebraic display.
In the future, there will also be buttons onscreen as an alternative.
Fractions and powers cannot be represented as a single token. Rather, fractions have a beginning, middle and end, and powers have a beginning and end. The program traverses the array to decide where to put them (and take them out for deletion).
The cursor
It is surprisingly difficult to make a vertical bar to act as a cursor in KaTeX, without affecting the spacing of the rest of the expression. For now, I am using a \clap{\rule{0.1em}{0.5em}} for the cursor, but I am looking to replace this with custom CSS in the future.
Replicating exact behaviour?
The fx-82 has some interesting behaviour wherein an empty power is not allowed to exist directly after another power. I am unsure whether I should replicate this - but for now, I am leaving it out.
Dealing with broken LaTeX
I have been able to get the display pretty bulletproof when it comes to not producing broken LaTeX. However, I’m sure there’s something I haven’t run into in debugging! I want to have this be dealt with gracefully rather than blowing up in the user’s face.
Immediate:
Medium:
Long-term (but getting closer!):
calc82: because calculators are for everyone
This devlog mainly follows the addition of fractional I/O to my calculator.
I defined a separate fraction data type: it’s like a decimal, but with a numerator and denominator, and optionally a whole component: for example, 1 and 1/3, or 4/3.
Using fractions means that numbers can be represented more naturally and precisely. I.e. 1/7 instead of 0.142857143
Additionally calculations can be made more precise. For example (1/7) * (2/3) can be directly computed by multiplying the numerators and denominators.
My implementation attempts to convert user input and the products of divisions into fractions wherever possible in order to chase this precision.
Web design is not my thing
I realised the importance of UI and UX (user experience) by looking at other projects on Stardance. As of the current iteration, this project lacks this a bit..
But I will give it a shot
My next developments will begin to focus more on this - starting with the algebraic input, and eventually moving onto the CSS and design aspects.
I am also considering whether I should use a Javascript framework.
Immediate:
Medium:
Long-term (but getting closer!):
calc82: because calculators are for everyone
Oh dear, looks like I lost 4 hours of counted time in the last devlog… let’s try to make this one a bit shorter!
This devlog follows the implementation of the other inline functions of the calculator:
Implementing most of this functionality followed the same general steps:
As I undertook this process. other dependent requirements popped up - such as the implementation of multivariable functions, which in turn required the expansion of the tokeniser and parser.
Also, I implemented degrees, radians, and gradians modes into the calculator, and the associated functions (as postfix expressions - e.g. sin(30d) for degrees).
Finally, I applied some further changes to decimal.js.:
Remembering that Javascript is weird
NaN === NaN is FALSE! Spent a good 10 minutes figuring this one out. The solution was to use isNaN().
Fixing negative-base powers:
(-8)^(1/3) should equal 2 but returns NaN
Fixed by taking the denominator from the output of a decimal.js toFraction() function, which attempts to create a rational fraction. This overcomes the tiny precision loss and evaluates the function correctly
Immediate:
Medium:
Long-term:
This has been a bit of a shorter devlog. But I’m still proud of what I achieved! Thanks so much for reading. If you have any suggestions, please leave me a comment. Take care
calc82: because calculators are for everyone
This devlog follows the handling of nasty floating point errors in my web calculator - inspired by and designed to work with Casio’s fx-82au.
I realised that relying purely on the Javascript math system was unsuitable for a scientific calculator that works with decimal numbers. This is due to binary floating point not translating well to decimal.
0.1+0.2=0.30000000000000004 >:(
I researched and considered a few approaches.
The idea behind this is to perform algebra and avoid evaluating constants and functions to values until strictly necessary. However, the scope of this seemed massive, and I did not observe this behaviour in the Casio.
Instead, I stumbled upon the fact that decimal arithmetic can be implemented in a computer. While IEEE floats use binary representation, there is no reason we cannot use decimals internally, avoiding errors that arise from conversion. Decimal floats can be stored by:
I started implementation, made decent progress, but found out that I was re-doing work that was already done by other people.
I found the decimal.js library after researching how to handle decimal addition. Implementing this quickly fixed my floating point errors, but did not fix trig approximation.
I observed some of the behaviour of the real calculator to work out how it functions.
I tried my best to implement the behaviour I observed, some of which required editing the code of the decimal.js library.
The decimal.js library uses more than the set precision for some internal calculations. However, arithmetic with pi is otherwise only the set precision, causing drift.
My fix for this was to adjust the internal pi value to match the mantissa size of the real calculator.
When given a less accurate value for pi (in line with that of the real calculator) sine quadrant is incorrectly calculated. I found that the isOdd function is flawed!
function isOdd(n) {
return n.d[n.d.length - 1] & 1;
}
I replaced it with one that works for my use case (if less efficient):
function isOdd(n) {
return n.mod(2).eq(1);
}
I need to see if this is just due to my low precision pi, or an actual problem in the library, in the future. But for now my issue is fixed.
I also found an error in the cosine function: if (x.isZero()) return x needed to be changed to if (x.isZero()) return new Ctor(1).
Again, I’m going to have to look into this more before submitting a pr or anything, since decimal.js is not made for such an imprecise pi.
Deviations
There are still small deviations between my calculator and the real one at really, really large trig angles.
One idea to reduce error could be optimising the order of operations in the AST to prevent dropping precision unnecessarily. I might explore this later
However, I want to press on with other features, instead of tiny inaccuracies that would never show up in real-world use. I have slightly rearranged the to-do list below.
Immediate:
Medium:
Long-term:
Thanks so much for reading! If you have any suggestions, please leave me a comment. Until next time
calc82: because calculators are for everyone
The aim of this project is to create a free, software-based browser calculator which mimics the behaviour of the Casio fx-82au. This is the most common calculator in high schools in my country.
My motivation for this project is the following:
I will take a black-box reverse engineering approach. This means analysing the inputs and outputs of the calculator and attempting to replicate the behaviour without looking at the underlying code. This along with avoiding patents and copyright should keep the project okay legally.
The final goal for the project is to create a fully functional calculator (obviously) which works on most types of devices. This means both touch and keyboard input, resolution scaling, wide browser support…
I have moderate experience with creating web apps. and a lot of experience using this calculator. so this project should be pretty fun :)
This devlog covers the initial development of the parts of the calculator that do math. Essentially turning an input expression into an output.
I chose a high-level structure similar to that used by programming language syntax analysers. That is:
Input -> Tokeniser -> Parser -> Evaluator -> Output
yes, with an s >:(
The tokeniser converts the input into a list of tokens (think individual pieces of the expression). It does this by scanning the input string and identifying what each character corresponds to. The tokeniser only knows what each symbol is, not what it does. Then it passes the token array to the…
The parser converts the token array into an Abstract Syntax Tree (AST). Essentially this is a tree which shows how the expressions should be evaluated in order to produce the desired output. This properly lets me implement order of operations.
The exact type of parser I am using is a recursive descent parser. It attempts to match the tokens with known symbols recursively, meaning i can rely on the call stack :)
The order of operations can be defined by the order of function calls within the parser.
The evaluator’s job is easy: it just traverses the tree made by the parser and runs the necessary operations. Because all of the hard work regarding order of operations has been done by the other components, implementing the evaluator was really easy. Its also recursive. your call stack is gonna love me for this
Why not eval?
It’s true that JavaScript has a perfectly good expression parser, which I could have used. However, its behaviour is more suited to a programming language than a calculator.
For example, 4(2+3) should implicitly multiply, but will instead complain TypeError: 4 is not a function. Writing my own system gives me more flexibility to make the calculator how I want.
Floating point precision
Because it uses Javascript’s internal math engine, i have floating-point problems. This results in expressions like
sin(2pi) = -2.44929360e-16
I will likely fix this by using a math library, or a novel approach.
Last-minute catch
I was just writing an example for this devlog when I found that .3ln(4)*5^2 raised an error. In fact, ANYTHING * or / followed by x^n would error - but other operators wouldn’t. This was a quick fix, but I’m glad I caught it!
Immediate:
Medium:
Long-term:
Thanks so much for reading! If you have any suggestions, please leave me a comment