Binary Calculator
toggle TOC (ctrl + ⇔)
Source on github
Many programming contests require performance critical solutions (like
many real life applications), which is often provided by clever manipulation
of bit arrays. I made this calculator to practice bit manipulation and familiarize
myself with more C++ along the way.
See the github repository for the latest description of the project; this page is out of date.
Fullscreen bincalc
The Usual Tricks ¶
basics | ||
---|---|---|
x ^ 0 = x |
x & 0 = 0 |
x | 0 = x |
x ^ ~0 = ~x |
x & ~0 = x |
x | ~0 = ~0 |
x ^ x = 0 |
x & x = x |
x | x = x |
get, set, go | ||
get ith bit |
x & (1 << i) |
|
set ith bit |
x | (1 << i) |
|
clear ith bit |
x & ~(1 << i) |
|
filter out bits above i |
x & ((1 << i) - 1) |
|
filter out bits below i |
x & ~((1 << i) - 1) |
|
extract n bits starting above i |
x & (~(~0 << n) << i) |
|
least significant bit | ||
clear least significant bit |
x & (x - 1) |
|
check if x is a power of 2 |
x & (x - 1) == 0 |
Instructions ¶
- Clone code from github or using cmd `git clone git://github.com/LemonPi/bincalc.git`
- Try **bc-windows** on windows, and on **bc-linux** on linux, else build it by following the rest of the steps
- Build with `make` in the directory if you have g++(gcc) with c++11 support
- Else either change (CC) in the makefile to your compiler of choice, or build with another tool
- Run with `./bc` or `bc` on windows in terminal
- Tips:
- exit with `Ctrl + c`
- adjust how many binary digits is shown by changing `bit_num` in consts.h
- 0xnum is **hex**, 0num is **oct**, and bnum is **binary**
- change underlying type (default is double) by changing `rep_type` in consts.h
Example usage (bc is directly called because I copied it to /usr/local/bin)
First Version - Stack based ¶
- stack based, requires users to use reverse polish notation (2 3 + to express 2 + 3)
- full support for `+, -, *, /, >>, <<, &, |, ^, and ~` operators
- no support for order of operations
- no support for variables
- supports only positive integers
- crash and memory leak free
- 1 file, ~100 lines
Restricting users to RPN input and not having variables were the biggest usability flaws. The largest technical weakness is its lack of modularity in keeping everything (reading from input, processing input, error handling) in one giant function. This makes it hard add features since everything depends on everything else in the function and order matters.
Second Version - Grammar added ¶
- stream based, conventional grammar
- full support for `+, -, *, /, >>, <<, &, |, ^, and ~` operators
- full support for order of operations
- support for variables
- supports only positive integers
- crash and memory leak free
- 1 file, ~200 lines
The largest usability flaw is restricting the user to positive integers (which is sufficient for bit manipulation practice). Many of the first version's flaws were a result of design rather than technical deficiency. Adding features to it would be hacky and difficult. I overhauled the design by introducing grammar, which naturally implements order of operations. Rules of grammar:
- everything is an expression
- every expression is a term optionally `+/-` other terms
- every term is a bit_term optionally `*//` other bit_terms
- every bit_term is a unary_term optionally `>>/<&/|/^` other unary_terms
- every unary_term is a primary optionally ~/literal itself (later I will add negative, hex, and oct)
- every primary is either a number, name, parentheses enclosed expression, or a unary operator symbol
The grammar is intuitively and efficiently handled by a recursive descent parser
(inspired by Bjorne Stroustrup's desk calculator in The C++ Language 4th edition, chapter 10.2).
Each level of the parser calls the next,
with each level applying its rules and returning its value.
For example, in expr()
, it expects at least a base term
rep_type left = term(need_get);
and adds or subtracts all additional terms it can
Separation of concerns and keeping it modular makes adding features and
understanding the code much easier. Here, expr()
doesn't have to worry about
the implementation at the lowest level, it just needs term()
to follow the
rules and return the value of a term. This sort of intuitive logic propagates
all the way down to the lowest level, prim()
, where actual parsing of values happen.
prim()
ultimately needs to tokens to parse, which comes from the lexer.
The lexer's job is to process state and value information from a input stream into tokens.
The other components include a table/dictionary to hold name-value information for variables, a simple error handling function, and a driver to start the parser.
Third Version - Files separated ¶
- stream based, conventional grammar
- full support for `+, -, *, /, %, >>, <<, &, |, ^, ~, !, and **` operators
- full support for order of operations
- support for variables
- supports real numbers with decimal, octal, and hexadecimal bases
- crash and memory leak free
- 12 file, ~300 lines
The missing features of the second version were easily added due to the modular design. The feature complete second version had no usability flaws, but it could be improved technically by enforcing modularity further from separation of functions to separation of files. The interface could also be removed from the implementation by separating header and source files.
As the diagram shows, each part relies only on other parts' interface, never their implementation. Version 2 roughly had this, but version 3 strictly enforces it. The conceptual benefits include more abstraction (without overhead) and modularity, making it easy to understand and add features. The technical benefits include stronger error safety and faster compile time, since files that haven't changed doesn't need to be recompiled. These benefits are much more important as project sizes scale up.
Gains from Experience ¶
- C++ experience
- Language processing experience
- Appreciation for C++'s offering of abstraction without overhead (fast and elegant code)
- Bit manipulation practice