Skip to content

Commit 997d521

Browse files
committed
fix(percentage): handle taxes
1 parent 57a24b3 commit 997d521

File tree

4 files changed

+67
-20
lines changed

4 files changed

+67
-20
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.DS_Store
12
.nyc_output/
23
coverage/
34
bin/

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ try {
1919
const value = evalExpr("100 + (3 * 55");
2020
} catch (e) {
2121
// Will throw an exception for invalid expressions
22-
console.error(e.message)
22+
console.error(e.message);
2323
}
2424
```
2525

2626
You can use basic operations (e.g. +, -, \*, /, %, ^ etc.) in you expressions.
2727

2828
### Examples
2929

30-
- 175 + 175 \* 18% + 40 = 246.5
30+
- 175 + 40 + 18% = 253.7
3131
- 10 + (100 - 100 \* 18%) \* 5 + 40 / 20 = 422
3232

3333
> _NOTE_: Floating points must be handled carefully. Use `Number(<number>).toFixed(<digits>)` to fix the digits after
@@ -48,7 +48,7 @@ This is the default and only export from this package.
4848
* @return number - The evaluated value.
4949
*
5050
* @throws Will throw an error if the expression in invalid.
51-
*
51+
*
5252
* NOTE: When passing floating points, please handle the digits after decimal point
5353
* e.g. 2 - 1.1 leads to 0.8999999999999999 intead of .9
5454
*/

src/index.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ describe("Evaluate Numerical Expressions", () => {
1515
expect(evalNumExpr("1 +1 +2 * 4/2")).toBe(6);
1616
});
1717

18+
it("Handle % for tax", () => {
19+
expect(evalNumExpr("10+18%")).toBe(11.8);
20+
expect(evalNumExpr("10+10+18%")).toBe(23.6);
21+
expect(evalNumExpr("100+(10+10+18%)")).toBe(123.6);
22+
expect(evalNumExpr("175 + 40 + 18%")).toBe(253.7);
23+
});
24+
it("Handle % for percentage", () => {
25+
expect(evalNumExpr("2*5+6%2")).toBe(10.12); // 2 * 5 + 6/100 * 2
26+
expect(evalNumExpr("10*6%")).toBe(0.6);
27+
expect(evalNumExpr("10 + 10*5%")).toBe(10.5);
28+
expect(evalNumExpr("10 + (100 - 100 * 18%) * 5 + 40 / 20")).toBe(422);
29+
});
30+
1831
it("Handles missing operands at end", () => {
1932
expect(evalNumExpr(" 5 + 5 *")).toBe(10);
2033
expect(evalNumExpr(" 5 + 5 /")).toBe(10);

src/index.ts

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ export default function evalNumExpr(amount: string): number {
33
const operators = [
44
new Operator("-", 2),
55
new Operator("+", 2),
6+
new Operator("add_percentage", 2),
7+
new Operator("sub_percentage", 2),
68
new Operator("/", 3),
79
new Operator("*", 3),
810
new Operator("^", 4),
911
];
1012
const tokens = tokenize(amount);
11-
const outputQueue = new Queue<string>();
13+
const outputQueue = new Queue<string | number>();
1214
const operatorsStack = new Stack<OperatorFns | "(" | ")">();
1315
/**
1416
* https://en.wikipedia.org/wiki/Shunting-yard_algorithm
@@ -100,7 +102,14 @@ export default function evalNumExpr(amount: string): number {
100102
return values.pop();
101103
}
102104

103-
type OperatorFns = "-" | "+" | "/" | "*" | "^";
105+
type OperatorFns =
106+
| "-"
107+
| "+"
108+
| "/"
109+
| "*"
110+
| "^"
111+
| "add_percentage"
112+
| "sub_percentage";
104113

105114
class Operator {
106115
static readonly LEFT_ASSOCIATIVE = 0;
@@ -118,6 +127,8 @@ class Operator {
118127
case "+":
119128
case "*":
120129
case "/":
130+
case "add_percentage":
131+
case "sub_percentage":
121132
this.associativity = Operator.LEFT_ASSOCIATIVE;
122133
break;
123134
case "^":
@@ -146,6 +157,10 @@ class Operator {
146157
return (operandA || 0) + (operandB || 0);
147158
case "-":
148159
return (operandA || 0) - (operandB || 0);
160+
case "add_percentage":
161+
return (operandA || 0) + ((operandA || 0) * (operandB || 0)) / 100;
162+
case "sub_percentage":
163+
return (operandA || 0) - ((operandA || 0) * (operandB || 0)) / 100;
149164
case "*":
150165
return (
151166
(typeof operandA !== "undefined" ? operandA : 1) *
@@ -170,33 +185,51 @@ class Operator {
170185
}
171186
}
172187

173-
function tokenize(amount: string): Array<OperatorFns | "(" | ")" | string> {
188+
function tokenize(
189+
amount: string
190+
): Array<OperatorFns | "(" | ")" | string | number> {
174191
// sanitize the amount
175192
amount = amount
176193
.replace(/\s/gi, "") // replace all spaces
177-
.replace(/%/gi, "/100") // replace percentage; TODO: Add modulus support as well
178194
.replace(/[xX]/gi, "*") // replace the x with multiplier *
179-
.replace(/[^+-/*^\d().]/g, "")
195+
.replace(/[^+-/*^\d().%]/g, "")
196+
.replace(/\+(\d+)%(?![*/(\d])/gi, "add_percentage$1")
197+
.replace(/-(\d+)%(?![*/(\d])/gi, "sub_percentage$1")
198+
.replace(/%([\d(])/gi, "/100*$1")
199+
.replace(/%/gi, "/100")
180200
// i have no idea why commas were not replaced :(
181201
// it works in Test Env (NodeJs)
182202
.replace(/,/g, "");
183-
const keywords = ["-", "+", "/", "*", "^", "(", ")"];
203+
const keywords = ["-", "+", "/", "*", "^", "%", "(", ")"];
184204
const tokens = [];
185-
let numberValue: string = "";
205+
let token: string = "";
186206
for (let i = 0; i < amount.length; i++) {
187207
const char = amount[i];
188-
if (keywords.indexOf(char) !== -1) {
189-
if (numberValue) {
190-
tokens.push(numberValue);
191-
numberValue = "";
192-
}
193-
tokens.push(char);
194-
} else {
195-
numberValue += char;
208+
const nextChar = amount[i + 1] || "";
209+
token += char;
210+
switch (true) {
211+
case !isNaN(Number(token)) &&
212+
Number.isFinite(Number(token)) &&
213+
isNaN(Number(nextChar)) &&
214+
nextChar !== ".":
215+
tokens.push(Number(token));
216+
token = "";
217+
break;
218+
case keywords.indexOf(token) !== -1:
219+
tokens.push(token);
220+
token = "";
221+
break;
222+
case token === "add_percentage":
223+
case token === "sub_percentage":
224+
tokens.push(token);
225+
token = "";
226+
break;
227+
default:
228+
break;
196229
}
197230
}
198-
if (numberValue) {
199-
tokens.push(numberValue);
231+
if (token) {
232+
tokens.push(token);
200233
}
201234
return tokens;
202235
}

0 commit comments

Comments
 (0)