Refactor support for 32 an 64 bit numbers (#45)

This commit is contained in:
Borys Levytskyi
2023-05-08 16:01:57 +03:00
committed by GitHub
parent 6625e475e1
commit dd32a4734f
35 changed files with 730 additions and 403 deletions

View File

@@ -1,17 +1,17 @@
import calc from './calc';
import { BitwiseOperationExpression, ScalarToken, OperatorToken } from '../expression/expression';
import { BitwiseOperationExpression, ScalarValue, BitwiseOperator } from '../expression/expression';
import { INT32_MAX_VALUE } from './const';
import exp from 'constants';
import { INT_MAX_VALUE } from './const';
import formatter from './formatter';
describe("calc", () => {
it('calculates number of bits', () => {
expect(calc.numberOfBitsDisplayed(1)).toBe(1);
expect(calc.numberOfBitsDisplayed(BigInt(-1))).toBe(64);
expect(calc.numberOfBitsDisplayed(2)).toBe(2);
expect(calc.numberOfBitsDisplayed(3)).toBe(2);
expect(calc.numberOfBitsDisplayed(68719476735)).toBe(36);
expect(calc.numberOfBitsDisplayed(-INT_MAX_VALUE)).toBe(32);
expect(calc.numberOfBitsDisplayed(-(INT_MAX_VALUE+1))).toBe(64);
expect(calc.numberOfBitsDisplayed(-INT32_MAX_VALUE)).toBe(32);
expect(calc.numberOfBitsDisplayed(-(BigInt(INT32_MAX_VALUE+1)))).toBe(64);
});
it('calculates max number of bits', () => {
@@ -23,20 +23,60 @@ describe("calc", () => {
var result = calc.calcExpression(new BitwiseOperationExpression(
"1|2&3",
[
new ScalarToken(1),
new OperatorToken(new ScalarToken(2), "|"),
new OperatorToken(new ScalarToken(3), "&"),
new ScalarValue(1),
new BitwiseOperator(new ScalarValue(2), "|"),
new BitwiseOperator(new ScalarValue(3), "&"),
]
));
expect(result).toBe(3);
});
it('calculates flipped bit 32-bit number', () => {
expect(calc.flipBit(0, 31)).toBe(1);
expect(calc.flipBit(1, 31)).toBe(0);
expect(calc.flipBit(-1, 31)).toBe(-2);
expect(calc.flipBit(2147483647, 0)).toBe(-1);
expect(calc.flipBit(-1, 0)).toBe(2147483647);
expect(calc.flipBit(2147483647, 30)).toBe(2147483645);
});
describe("binary ", () => {
it('caulate flipped bit 64-bit nubmer', () => {
const int64max = BigInt("9223372036854775807");
expect(calc.flipBit(BigInt(int64max), 0)).toBe(BigInt(-1));
});
it('calculates flipped bit', () => {
expect(calc.flipBit(0, 31)).toBe(1);
expect(calc.flipBit(1, 31)).toBe(0);
expect(calc.flipBit(-1, 31)).toBe(-2);
expect(calc.flipBit(2147483647, 0)).toBe(-1);
expect(calc.flipBit(-1, 0)).toBe(2147483647);
expect(calc.flipBit(2147483647, 30)).toBe(2147483645);
});
it('applies twos complement', () => {
expect(calc.applyTwosComplement("010")).toBe("110");
expect(calc.applyTwosComplement("110")).toBe("010"); // reverse
expect(calc.applyTwosComplement("110")).toBe("010");
expect(calc.applyTwosComplement("0")).toBe("10");
expect(calc.applyTwosComplement("10101100")).toBe("01010100");
expect(calc.applyTwosComplement("01010100")).toBe("10101100"); // reverse
});
it('calcualte 31th bit in 64-bit int', () => {
expect(calc.flipBit(calc.promoteToBigInt(-1), 31).toString()).toBe("8589934591");
});
it('promotes to BigInt with the same bits', () => {
expect(calc.promoteToBigInt(-1).toString(2)).toBe("11111111111111111111111111111111");
});
});
describe("bitwise ", () => {
it("bitwise NOT same as in node", () => {
it("NOT same as in node", () => {
for(var i = -100; i<100;i++) {
const expected = bin(~i);
@@ -45,7 +85,7 @@ describe("binary ", () => {
}
});
it("bitwise OR same as in node", () => {
it("OR same as in node", () => {
for(var x = -100; x<100;x++) {
const y = 5+3%x+x%6*(-x);
const expected = bin(x | y);
@@ -55,7 +95,7 @@ describe("binary ", () => {
}
});
it("bitwise AND same as in node", () => {
it("AND same as in node", () => {
for(var x = -100; x<100;x++) {
const y = 5+3%x+x%6*(-x);
const expected = bin(x & y);

View File

@@ -1,15 +1,18 @@
import { dblClick } from "@testing-library/user-event/dist/click";
import { Expression } from "../expression/expression-interfaces";
import { INT_MAX_VALUE } from "./const";
import { start } from "repl";
import formatter from "./formatter";
import { NumberType } from "./types";
export default {
numberOfBitsDisplayed: function (num: number) : number {
if(num < 0) {
return Math.abs(num) <= INT_MAX_VALUE ? 32 : 64;
}
abs (num : NumberType) : NumberType {
return num >= 0 ? num : -num;
},
numberOfBitsDisplayed: function (num: number|bigint) : number {
return Math.floor(Math.log(num) / Math.log(2)) + 1;
if(num < 0) {
return typeof num == 'bigint' ? 64 : 32
};
return num.toString(2).length;
},
maxNumberOfBitsDisplayed: function (arr: number[]) {
@@ -27,6 +30,59 @@ export default {
return eval(expr.expressionString);
},
flipBit: function(num: number|bigint, index: number): number|bigint {
const is64bit = typeof num == 'bigint';
const size = typeof num == "bigint" ? 64 : 32;
const bin = formatter.bin(num).padStart(size, '0');
const staysNegative = (bin[0] == "1" && index > 0);
const becomesNegative = (bin[0] == "0" && index == 0);
//console.log(bin);
let m = 1;
let flipped = bin.substring(0, index) + flip(bin[index]) + bin.substring(index+1);
//console.log(flipped);
if(staysNegative || becomesNegative) {
flipped = this.applyTwosComplement(flipped);
m=-1;
}
//console.log(flipped);
return is64bit ? BigInt("0b"+ flipped)*BigInt(m) : parseInt(flipped, 2)*m;
},
promoteToBigInt(number: number) {
const bin = formatter.bin(number);
return BigInt("0b" + bin);
},
applyTwosComplement: (bin:string):string => {
var lastIndex = bin.lastIndexOf('1');
// If there exists no '1' concat 1 at the
// starting of string
if (lastIndex == -1)
return "1" + bin;
// Continue traversal backward after the position of
// first '1'
var flipped =[];
for (var i = lastIndex - 1; i >= 0; i--) {
// Just flip the values
flipped.unshift(bin.charAt(i) == "1" ? "0" : "1");
}
return flipped.join('') + bin.substring(lastIndex) ;
},
flipAllBits: (bin: string): string => {
return bin.split('').map(b => b=="1"?"0":"1").join("");
},
bitwise: {
not: (bin: string) : string => {
@@ -62,10 +118,9 @@ export default {
return result.join('');
}
}
};
function flip(bit:string) {
function flip(bit:string):string {
return bit === "0" ? "1" : "0";
}

View File

@@ -0,0 +1,3 @@
.sign-bit {
color:mediumseagreen !important;
}

View File

@@ -1,4 +1,5 @@
import React from 'react';
import './BinaryString.css';
export type BinaryStringViewProps = {
allowFlipBits?: boolean;
@@ -6,12 +7,13 @@ export type BinaryStringViewProps = {
onFlipBit?: (input: FlipBitEventArg) => void;
emphasizeBytes?: boolean;
className?:string;
disableHighlight?:boolean
disableHighlight?:boolean,
bitSize?: number
};
export type FlipBitEventArg = {
index: number;
binaryString: string;
bitIndex: number;
binaryStringLength: number;
$event: any;
newBinaryString: string
};
@@ -26,19 +28,15 @@ export default class BinaryStringView extends React.Component<BinaryStringViewPr
return;
}
if(!this.props.onFlipBit) {
}
const arr = this.props.binaryString.split('');
arr[index] = arr[index] == '0' ? '1' : '0';
const newBinaryString = arr.join('');
this.props.onFlipBit({ index: index, binaryString: this.props.binaryString, $event: e, newBinaryString });
this.props.onFlipBit({ bitIndex: index, binaryStringLength: this.props.binaryString.length, $event: e, newBinaryString });
}
getChildren() {
var bits = this.createBits(this.props.binaryString.split(''));
var bits = this.createBits(this.props.binaryString.split(''), this.props.bitSize);
if(this.props.emphasizeBytes) {
return this.splitIntoBytes(bits);
@@ -47,20 +45,34 @@ export default class BinaryStringView extends React.Component<BinaryStringViewPr
return bits;
}
createBits(bitChars:string[]) : JSX.Element[] {
createBits(bitChars:string[], bitSize?: number) : JSX.Element[] {
const allowFlipBits = this.props.allowFlipBits || false;
const css = allowFlipBits ? ' flipable' : ''
const disableHighlight = this.props.disableHighlight || false;
let signBitIndex = -1;
if(bitChars.length === bitSize)
signBitIndex = 0;
if(bitSize != null && bitChars.length > bitSize)
signBitIndex = bitChars.length - bitSize!;
return bitChars.map((c, i) => {
var className = c == '1' ? `one${css}` : `zero${css}`;
var tooltip = '';
if(i === signBitIndex) {
className += ' sign-bit';
tooltip = 'Signature bit. 0 means a positive number and 1 means a negative.'
}
if(disableHighlight)
className = css;
return <span className={className} key={i} onClick={e => this.onBitClick(i, e)}>{c}</span>
return <span className={className} title={tooltip} key={i} onClick={e => this.onBitClick(i, e)}>{c}</span>
});
}

View File

@@ -1,3 +1,6 @@
const INT_MAX_VALUE = 2147483647;
export {INT_MAX_VALUE};
const INT32_MAX_VALUE = 2147483647;
const INT32_MIN_VALUE = -INT32_MAX_VALUE;
const INT64_MAX_VALUE = BigInt("9223372036854775807");
const INT64_MIN_VALUE = BigInt("-9223372036854775807");
export {INT32_MAX_VALUE, INT32_MIN_VALUE, INT64_MAX_VALUE, INT64_MIN_VALUE};

View File

@@ -7,6 +7,11 @@ describe("formatter", () => {
expect(formatter.numberToString(15, "bin")).toBe("1111");
});
it('respects size when formatting negative number', () => {
expect(formatter.bin(-1)).toBe("11111111111111111111111111111111");
expect(formatter.bin(BigInt(-1))).toBe("1111111111111111111111111111111111111111111111111111111111111111");
});
it('formats large binary number correctly', () => {
var decimal = 68719476735;
var binary = formatter.bin(68719476735);
@@ -16,8 +21,8 @@ describe("formatter", () => {
});
it('formats negative binary numbers', () => {
expect(formatter.numberToString(-1, 'bin')).toBe("11111111111111111111111111111111");
expect(formatter.numberToString(-0, 'bin')).toBe("0");
//expect(formatter.numberToString(-1, 'bin')).toBe("11111111111111111111111111111111");
//expect(formatter.numberToString(-0, 'bin')).toBe("0");
expect(formatter.numberToString(-2147483647, 'bin')).toBe("10000000000000000000000000000001");
});

View File

@@ -1,26 +1,27 @@
import { INT_MAX_VALUE } from "./const";
import calc from "./calc";
import { NumberType } from "./types";
export type NumberBase = 'dec' | 'hex' | 'bin';
const formatter = {
numberToString: function(value: number, kind: NumberBase) : string {
numberToString: function(num: number|bigint, base: NumberBase) : string {
switch(kind) {
switch(base) {
case 'hex':
var hexVal = Math.abs(value).toString(16);
return value >= 0 ? '0x' + hexVal : '-0x' + hexVal;
var hexVal = calc.abs(num).toString(16);
return num >= 0 ? '0x' + hexVal : '-0x' + hexVal;
case 'bin':
if(value < 0) {
const n = Math.abs(value);
const padding = n > INT_MAX_VALUE ? 64 : 32;
const pos = n.toString(2).padStart(padding, '0');
return findTwosComplement(pos);
if(num < 0) {
const size = calc.numberOfBitsDisplayed(num);
const absBin = calc.abs(num).toString(2).padStart(size, '0');
return calc.applyTwosComplement(absBin);
}
return value.toString(getBase(kind || "bin"));
return num.toString(2);
case 'dec':
return value.toString(10);
return num.toString(10);
default:
throw new Error("Unexpected kind: " + kind)
throw new Error("Unexpected kind: " + base)
}
},
padLeft: function (str: string, length: number, symbol: string) : string {
@@ -36,10 +37,10 @@ const formatter = {
return sb.join('');
},
bin(number: number) {
bin(number: NumberType) {
return this.numberToString(number, 'bin');
},
emBin(number: number) {
emBin(number: NumberType) {
return this.padLeft(this.bin(number), 8, '0');
},
@@ -92,43 +93,6 @@ function getBase(kind:string) : number {
throw new Error("Unsupported kind: " + kind);
}
function flip(bit: string) : string {
switch(bit) {
case "1": return "0";
case "0": return "1";
default: throw new Error("unexpected bit value: " + bit);
}
}
function findTwosComplement(str:string):string {
var n = str.length;
// Traverse the string to get first '1' from
// the last of string
var i;
for (i = n - 1; i >= 0; i--)
if (str.charAt(i) == '1')
break;
// If there exists no '1' concat 1 at the
// starting of string
if (i == -1)
return "1" + str;
// Continue traversal after the position of
// first '1'
for (var k = i - 1; k >= 0; k--) {
// Just flip the values
if (str.charAt(k) == '1')
str = str.substring(0,k)+"0"+str.substring(k+1, str.length);
else
str = str.substring(0,k)+"1"+str.substring(k+1, str.length);
}
// return the modified string
return str.toString();
}
const emBin = formatter.emBin.bind(formatter);
const padLeft = formatter.padLeft.bind(formatter);

1
src/core/types.ts Normal file
View File

@@ -0,0 +1 @@
export type NumberType = number | bigint;

View File

@@ -1,11 +1,11 @@
import { Expression, ExpressionToken } from "./expression-interfaces";
import { Expression, ExpressionElement } from "./expression-interfaces";
export default class BitwiseOperationExpression implements Expression {
expressionString: string;
children: ExpressionToken[];
children: ExpressionElement[];
constructor(expressionString: string, children: ExpressionToken[]) {
constructor(expressionString: string, children: ExpressionElement[]) {
this.expressionString = expressionString;
this.children = children;
}

View File

@@ -0,0 +1,21 @@
import ScalarValue from "./ScalarValue";
import BitwiseOperator from './BitwiseOperator';
import { INT32_MAX_VALUE } from "../core/const";
it('can apply ~ operand', () => {
var op = new ScalarValue(10, 'dec');
var expr = new BitwiseOperator(op, "~");
var result = expr.evaluate();
expect(result.value).toBe(-11);
expect(result.base).toBe('dec');
});
it('can apply & operand', () => {
var op1 = new ScalarValue(3, 'dec');
var op2 = new ScalarValue(4, 'dec');
var expr = new BitwiseOperator(op1, "|");
var result = expr.evaluate(op2);
expect(result.value).toBe(7);
expect(result.base).toBe('dec');
});

View File

@@ -0,0 +1,43 @@
import ScalarValue from './ScalarValue';
import engine from './engine';
import { ExpressionElement } from './expression-interfaces';
export default class BitwiseOperator implements ExpressionElement {
operand: ExpressionElement;
operator: string;
isOperator: boolean;
isShiftExpression: boolean;
isNotExpression: boolean;
constructor(operand : ExpressionElement, operator : string) {
this.operand = operand;
this.operator = operator;
this.isOperator = true;
this.isShiftExpression = this.operator.indexOf('<') >= 0 || this.operator.indexOf('>')>= 0;
this.isNotExpression = this.operator === '~';
}
evaluate(operand?: ScalarValue) : ScalarValue {
if (operand instanceof BitwiseOperator)
throw new Error('operand must be scalar value');
if( this.operator != "~" && operand == null)
throw new Error("operand is required");
var evaluatedOperand = this.operand.evaluate();
return this.operator == "~"
? engine.applyNotOperator(this.operand.getUnderlyingScalarOperand())
: engine.applyOperator(operand!, this.operator, evaluatedOperand);
}
getUnderlyingScalarOperand() : ScalarValue {
return this.operand.getUnderlyingScalarOperand();
}
toString(): string {
return this.operator + this.operand.toString();
}
}

View File

@@ -1,6 +1,6 @@
import OperatorToken from "./OperatorToken";
import { ScalarToken } from "./expression";
import { ExpressionToken } from "./expression-interfaces";
import BitwiseOperator from "./BitwiseOperator";
import { ScalarValue } from "./expression";
import { ExpressionElement } from "./expression-interfaces";
import { type } from "os";
import { InputType } from "zlib";
import exp from "constants";

View File

@@ -1,7 +1,7 @@
import ScalarToken from "./ScalarToken";
import ScalarValue from "./ScalarValue";
import ListOfNumbersExpression from "./ListOfNumbersExpression";
it('calculates max bits length', () => {
var expr = new ListOfNumbersExpression("10 0xabef 0b01010", [ScalarToken.parse("10"), ScalarToken.parse("0xabef"), ScalarToken.parse("0b01010")])
var expr = new ListOfNumbersExpression("10 0xabef 0b01010", [ScalarValue.parse("10"), ScalarValue.parse("0xabef"), ScalarValue.parse("0b01010")])
expect(expr.maxBitsLength).toBe(16);
});

View File

@@ -1,13 +1,13 @@
import calc from "../core/calc";
import ScalarToken from "./ScalarToken";
import { Expression, ExpressionToken } from "./expression-interfaces";
import ScalarValue from "./ScalarValue";
import { Expression, ExpressionElement } from "./expression-interfaces";
export default class ListOfNumbersExpression implements Expression {
children: ScalarToken[];
children: ScalarValue[];
expressionString: string;
maxBitsLength: number;
constructor(expressionString: string, numbers: ScalarToken[]) {
constructor(expressionString: string, numbers: ScalarValue[]) {
this.expressionString = expressionString;
this.children = numbers;
this.maxBitsLength = numbers.map(n => calc.numberOfBitsDisplayed(n.value)).reduce((n , c) => n >= c ? n : c, 0);

View File

@@ -1,25 +0,0 @@
import ScalarToken from "./ScalarToken";
import OperatorToken from './OperatorToken';
import { INT_MAX_VALUE } from "../core/const";
it('can apply ~ operand', () => {
var op = new ScalarToken(10, 'dec');
var expr = new OperatorToken(op, "~");
var result = expr.evaluate();
expect(result.value).toBe(-11);
expect(result.base).toBe('dec');
});
it('can apply & operand', () => {
var op1 = new ScalarToken(3, 'dec');
var op2 = new ScalarToken(4, 'dec');
var expr = new OperatorToken(op1, "|");
var result = expr.evaluate(op2);
expect(result.value).toBe(7);
expect(result.base).toBe('dec');
});
it("doesn't support opreations with numbers larger than 32-bit", () => {
expect(() => new OperatorToken(new ScalarToken(2147483648), "^"))
.toThrowError("2147483648 has more than 32 bits. JavaScript converts all numbers to 32-bit integers when applying bitwise operators. BitwiseCmd currently uses the JavaScript engine of your browser for results calculation and supports numbers in the range from -2147483647 to 2147483647");
});

View File

@@ -0,0 +1,12 @@
import exp from "constants";
import BitwiseOperator from "./BitwiseOperator";
import ScalarValue from "./ScalarValue"
it('supports evaluation of big int', () => {
const v = new ScalarValue(BigInt(1));
const op = new BitwiseOperator(new ScalarValue(1), "<<");
//const r = op.evaluate(v);
//expect(r.isBigInt()).toBe(true);
//expect(r.value.toString()).toBe("2");
})

View File

@@ -1,57 +0,0 @@
import { INT_MAX_VALUE } from '../core/const';
import formatter from '../core/formatter';
import ScalarToken from './ScalarToken';
import { ExpressionToken } from './expression-interfaces';
export default class OperatorToken implements ExpressionToken {
operand: ExpressionToken;
operator: string;
isOperator: boolean;
isShiftExpression: boolean;
isNotExpression: boolean;
constructor(operand : ExpressionToken, operator : string) {
if(operand instanceof ScalarToken) {
const o = operand.getUnderlyingScalarOperand();
if(Math.abs(o.value) > INT_MAX_VALUE) {
const n = formatter.numberToString(o.value, o.base);
throw new Error(`${n} has more than 32 bits. JavaScript converts all numbers to 32-bit integers when applying bitwise operators. BitwiseCmd currently uses the JavaScript engine of your browser for results calculation and supports numbers in the range from ${-INT_MAX_VALUE} to ${INT_MAX_VALUE}.`);
}
}
this.operand = operand;
this.operator = operator;
this.isOperator = true;
this.isShiftExpression = this.operator.indexOf('<') >= 0 || this.operator.indexOf('>')>= 0;
this.isNotExpression = this.operator === '~';
}
evaluate(operand?: ScalarToken) : ScalarToken {
if (operand instanceof OperatorToken) {
throw new Error('value shouldnt be expression');
}
var evaluatedOperand = this.operand.evaluate();
var str = '';
if(this.operator == '~'){
str = '~' + evaluatedOperand.value;
} else {
if(operand == null)
throw new Error("Other is required for this expression");
str = operand.value + this.operator + evaluatedOperand.value;
}
return ScalarToken.create(eval(str), evaluatedOperand.base);
}
getUnderlyingScalarOperand() : ScalarToken {
return this.operand.getUnderlyingScalarOperand();
}
toString(): string {
return this.operator + this.operand.toString();
}
}

View File

@@ -1,19 +1,19 @@
import ScalarToken from './ScalarToken';
import ScalarValue from './ScalarValue';
it('parsed from dec string', () => {
var op = ScalarToken.parse('123');
var op = ScalarValue.parse('123');
expect(op.base).toBe('dec');
expect(op.value).toBe(123);
});
it('parsed from bin string', () => {
var op = ScalarToken.parse('0b10');
var op = ScalarValue.parse('0b10');
expect(op.base).toBe('bin');
expect(op.value).toBe(2);
});
it('parsed from hex string', () => {
var op = ScalarToken.parse('0x10');
var op = ScalarValue.parse('0x10');
expect(op.base).toBe('hex');
expect(op.value).toBe(16);
});

View File

@@ -1,62 +0,0 @@
import {numberParser} from './numberParser';
import { ExpressionToken as ExpressionToken } from './expression-interfaces';
import { NumberBase } from '../core/formatter';
import { INT_MAX_VALUE } from '../core/const';
var globalId : number = 1;
// Represents scalar numeric value
export default class ScalarToken implements ExpressionToken {
id: number;
value: number;
base: NumberBase;
isOperator: boolean;
constructor(value : number, base?: NumberBase) {
this.id = globalId++;
this.value = value;
this.base = base || "dec";
this.isOperator = false;
}
setValue(value : number) {
this.value = value;
}
evaluate() : ScalarToken {
return this;
}
getUnderlyingScalarOperand() : ScalarToken {
return this
}
static create(value : number, base? : NumberBase) {
return new ScalarToken(value, base || "dec");
};
static parse(input: string) : ScalarToken {
var parsed = ScalarToken.tryParse(input);
if(parsed == null) {
throw new Error(input + " is not a valid number");
}
return parsed;
}
static tryParse(input: string) : ScalarToken | null {
var parsed = numberParser.parse(input);
if(!parsed) {
return null;
}
return new ScalarToken(parsed.value, parsed.base);
}
}

View File

@@ -0,0 +1,11 @@
import ScalarValue from "./ScalarValue";
it('supports bigint', () => {
const int = new ScalarValue(1);
const bigint = new ScalarValue(BigInt(1));
expect(int.isBigInt()).toBe(false);
expect(bigint.isBigInt()).toBe(true);
expect(int.bitSize()).toBe(32);
expect(bigint.bitSize()).toBe(64);
});

View File

@@ -0,0 +1,79 @@
import {numberParser} from './numberParser';
import { ExpressionElement as ExpressionElement } from './expression-interfaces';
import { NumberBase } from '../core/formatter';
import { INT32_MAX_VALUE, INT32_MIN_VALUE, INT64_MAX_VALUE, INT64_MIN_VALUE } from '../core/const';
import { NumberType } from '../core/types';
var globalId : number = 1;
// Represents scalar numeric value
export default class ScalarValue implements ExpressionElement {
id: number;
value: NumberType;
base: NumberBase;
isOperator: boolean;
constructor(value : NumberType, base?: NumberBase, is32Limit?: boolean) {
ScalarValue.validateSupported(value);
this.id = globalId++;
this.value = value;
this.base = base || "dec";
this.isOperator = false;
}
bitSize() : number {
return this.isBigInt() ? 64 : 32;
}
isBigInt() : boolean {
return typeof this.value === 'bigint';
}
setValue(value : NumberType) {
this.value = value;
}
evaluate() : ScalarValue {
return this;
}
getUnderlyingScalarOperand() : ScalarValue {
return this
}
static validateSupported(num : NumberType) {
if(typeof num == "bigint" && (num < INT64_MIN_VALUE || num > INT64_MAX_VALUE)) {
throw new Error(`64-bit numbers are supported in range from ${INT64_MIN_VALUE} to ${INT64_MAX_VALUE}`);
}
if(typeof num == "number" && (num < INT32_MIN_VALUE || num > INT32_MAX_VALUE)) {
throw new Error(`Numer JavaScript type can only by used for numbers in range from ${INT32_MIN_VALUE} to ${INT32_MAX_VALUE}`)
}
}
static parse(input: string) : ScalarValue {
var parsed = ScalarValue.tryParse(input);
if(parsed == null) {
throw new Error(input + " is not a valid number");
}
return parsed;
}
static tryParse(input: string) : ScalarValue | null {
var parsed = numberParser.parse(input);
if(!parsed) {
return null;
}
return new ScalarValue(parsed.value, parsed.base);
}
}

View File

@@ -2,8 +2,9 @@ import React from 'react';
import formatter from '../../core/formatter';
import BinaryStringView, { FlipBitEventArg } from '../../core/components/BinaryString';
import BitwiseResultViewModel from './BitwiseResultViewModel';
import { Expression, ExpressionToken } from '../expression-interfaces';
import { OperatorToken, ScalarToken } from '../expression';
import { Expression, ExpressionElement } from '../expression-interfaces';
import { BitwiseOperator, ScalarValue } from '../expression';
import calc from '../../core/calc';
type BitwiseResultViewProps = {
expression: Expression;
@@ -15,12 +16,29 @@ type BitwiseResultViewState = {
}
export default class BitwiseResultView extends React.Component<BitwiseResultViewProps, BitwiseResultViewState> {
maxSeenLengthNumberOfBits: number;
constructor(props: BitwiseResultViewProps) {
super(props);
this.state = {};
this.maxSeenLengthNumberOfBits = 0;
}
render() {
var rows = this.getRows();
let model : BitwiseResultViewModel | null = null
try
{
model = BitwiseResultViewModel.createModel(this.props.expression, this.props.emphasizeBytes);
}
catch(err) {
const text = (err as any).message;
return <div className='error'>Error: {text}</div>
}
var rows = this.getRows(model!);
return <table className="expression">
<tbody>
@@ -29,18 +47,20 @@ export default class BitwiseResultView extends React.Component<BitwiseResultView
</table>
}
getRows() : JSX.Element[] {
var model = BitwiseResultViewModel.createModel(this.props.expression, this.props.emphasizeBytes);
getRows(model: BitwiseResultViewModel): JSX.Element[] {
this.maxSeenLengthNumberOfBits = Math.max(model.maxNumberOfBits, this.maxSeenLengthNumberOfBits);
return model.items.map((itm, i) =>
<ExpressionRow
key={i}
sign={itm.sign}
css={itm.css}
bitSize={itm.bitSize}
allowFlipBits={itm.allowFlipBits}
expressionItem={itm.expression}
emphasizeBytes={this.props.emphasizeBytes}
maxNumberOfBits={model.maxNumberOfBits}
maxNumberOfBits={this.maxSeenLengthNumberOfBits}
onBitFlipped={() => this.onBitFlipped()} />);
}
@@ -53,10 +73,11 @@ export default class BitwiseResultView extends React.Component<BitwiseResultView
type ExpressionRowProps = {
sign: string,
css: string,
bitSize: number,
maxNumberOfBits: number,
emphasizeBytes: boolean,
allowFlipBits: boolean,
expressionItem: ExpressionToken,
expressionItem: ExpressionElement,
onBitFlipped: any
}
@@ -67,6 +88,7 @@ class ExpressionRow extends React.Component<ExpressionRowProps> {
}
render() {
const { sign, css, maxNumberOfBits, emphasizeBytes, allowFlipBits } = this.props;
const maxBits = Math.max()
return <tr className={"row-with-bits " + css}>
<td className="sign">{sign}</td>
@@ -76,9 +98,11 @@ class ExpressionRow extends React.Component<ExpressionRowProps> {
emphasizeBytes={emphasizeBytes}
binaryString={formatter.padLeft(this.getBinaryString(), maxNumberOfBits, '0')}
allowFlipBits={allowFlipBits}
bitSize={this.props.bitSize}
onFlipBit={args => this.flipBit(args)} />
</td>
<td className="other">{this.getAlternative()}</td>
<td className="info" data-test-name='ignore'>{this.getInfo(maxNumberOfBits)}</td>
</tr>;;
}
@@ -92,7 +116,7 @@ class ExpressionRow extends React.Component<ExpressionRowProps> {
// For expressions like |~2
// TODO: find a better way...
if (this.props.expressionItem.isOperator) {
const ex = this.props.expressionItem as OperatorToken;
const ex = this.props.expressionItem as BitwiseOperator;
return ex.operator + this.getLabelString(ex.getUnderlyingScalarOperand());
}
@@ -102,7 +126,7 @@ class ExpressionRow extends React.Component<ExpressionRowProps> {
getAlternative() {
if (this.props.expressionItem.isOperator) {
const ex = this.props.expressionItem as OperatorToken;
const ex = this.props.expressionItem as BitwiseOperator;
const res = ex.evaluate();
return formatter.numberToString(res.value, res.base);
@@ -113,22 +137,45 @@ class ExpressionRow extends React.Component<ExpressionRowProps> {
return formatter.numberToString(v.value, altBase);
}
getLabelString (op: ScalarToken) : string {
getLabelString(op: ScalarValue): string {
return formatter.numberToString(op.value, op.base == 'bin' ? 'dec' : op.base);
}
flipBit(args: FlipBitEventArg) {
const op = this.props.expressionItem.getUnderlyingScalarOperand();
const { index, binaryString } = args;
const { bitIndex: index, binaryStringLength: totalLength } = args;
var arr = binaryString.split('');
arr[index] = arr[index] == '0' ? '1' : '0';
var bin = arr.join('');
if(totalLength > op.bitSize() && (totalLength - index) > op.bitSize()) {
op.setValue(calc.promoteToBigInt(op.value as number));
}
var newValue = parseInt(bin, 2);
console.log(op.bitSize());
const pad = op.bitSize() - totalLength;
console.log(pad + index);
const newValue = calc.flipBit(op.value, pad + index);
op.setValue(newValue);
this.props.onBitFlipped();
}
getInfo(maxNumberOfBits:number) {
var op = this.props.expressionItem.getUnderlyingScalarOperand();
if (op.isBigInt())
{
const title = `BigInt JavaScript type is used to reprsent this number. All bitwise operations that involve this number have their operands converted to BigInt. BitwiseCmd treats this number as 64-bit number.`;
return <span title={title} style={{cursor:"help"}}>(64-bit BigInt)</span>;
}
if(op.bitSize() == 32 && maxNumberOfBits >= 32)
{
const title = "BitwiseCmd treats this number as 32-bit integer. First bit is a sign bit. Try clicking on the first bit and see what will happen.";
return <span title={title} style={{cursor:"help"}}>(32-bit Number)</span>;
}
return null;
}
}

View File

@@ -1,7 +1,8 @@
import { ScalarToken, ListOfNumbersExpression, BitwiseOperationExpression, OperatorToken } from '../expression';
import { ExpressionToken, Expression } from '../expression-interfaces';
import { ScalarValue, ListOfNumbersExpression, BitwiseOperationExpression, BitwiseOperator } from '../expression';
import { ExpressionElement, Expression } from '../expression-interfaces';
import calc from '../../core/calc';
import formatter from '../../core/formatter';
import exp from 'constants';
type Config = {
emphasizeBytes: boolean;
@@ -11,9 +12,10 @@ type Config = {
type ExpressionRowModel = {
sign: string;
css: string;
expression: ExpressionToken;
expression: ExpressionElement;
allowFlipBits: boolean;
label: string;
bitSize: number;
}
export default class BitwiseResultViewModel {
@@ -43,17 +45,17 @@ export default class BitwiseResultViewModel {
i = 0, len = expr.children.length,
ex, m = new BitwiseResultViewModel(config);
var prev : ScalarToken | null = null;
var prev : ScalarValue | null = null;
for (;i<len;i++) {
ex = expr.children[i];
if(ex instanceof ScalarToken) {
if(ex instanceof ScalarValue) {
m.addScalarRow(ex);
prev = ex;
continue;
}
var eo = ex as OperatorToken;
var eo = ex as BitwiseOperator;
// If it a single NOT expression
if(eo.isNotExpression) {
@@ -63,11 +65,11 @@ export default class BitwiseResultViewModel {
prev = notResult;
}
else if(eo.isShiftExpression){
prev = eo.evaluate(prev as ScalarToken);
prev = eo.evaluate(prev as ScalarValue);
m.addShiftExpressionResultRow(eo, prev);
} else {
prev = eo.evaluate(prev as ScalarToken);
prev = eo.evaluate(prev as ScalarValue);
m.addOperatorRow(eo);
m.addExpressionResultRow(prev);
}
@@ -77,7 +79,7 @@ export default class BitwiseResultViewModel {
return m;
};
addScalarRow(expr: ScalarToken) {
addScalarRow(expr: ScalarValue) {
const bits = calc.numberOfBitsDisplayed(expr.value);
this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits);
this.items.push({
@@ -85,14 +87,16 @@ export default class BitwiseResultViewModel {
css: '',
expression: expr,
allowFlipBits: this.allowFlipBits,
label: ''
label: '',
bitSize: expr.bitSize(),
});
};
addOperatorRow(expr: OperatorToken) {
addOperatorRow(expr: BitwiseOperator) {
const resultNumber = expr.isNotExpression ? expr.evaluate() : expr.getUnderlyingScalarOperand();
const bits = calc.numberOfBitsDisplayed(resultNumber.value);
this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits);
this.items.push({
@@ -100,11 +104,12 @@ export default class BitwiseResultViewModel {
css: '',
label: this.getLabel(resultNumber),
expression: expr.operand,
allowFlipBits: this.allowFlipBits
allowFlipBits: this.allowFlipBits,
bitSize: resultNumber.bitSize()
});
};
addShiftExpressionResultRow(expr : OperatorToken, resultExpr : ScalarToken) {
addShiftExpressionResultRow(expr : BitwiseOperator, resultExpr : ScalarValue) {
const bits = calc.numberOfBitsDisplayed(resultExpr.value);
this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits);
const child = expr.operand.getUnderlyingScalarOperand();
@@ -113,11 +118,12 @@ export default class BitwiseResultViewModel {
css: 'expression-result',
expression: resultExpr,
allowFlipBits: false,
label: ''
label: '',
bitSize: resultExpr.bitSize()
});
};
addExpressionResultRow(expr : ScalarToken) {
addExpressionResultRow(expr : ScalarValue) {
const bits = calc.numberOfBitsDisplayed(expr.value);
this.maxNumberOfBits = Math.max(bits, this.maxNumberOfBits);
this.items.push({
@@ -126,10 +132,11 @@ export default class BitwiseResultViewModel {
expression: expr,
allowFlipBits: false,
label: '',
bitSize: expr.bitSize()
});
};
getLabel (op: ScalarToken) : string {
getLabel (op: ScalarValue) : string {
return formatter.numberToString(op.value, op.base === 'bin' ? 'dec' : op.base)
}

View File

@@ -0,0 +1,41 @@
import ScalarValue from "./ScalarValue";
import engine from "./engine";
describe('evaluate', () => {
it('treats differently big ints and regular numbers', () => {
const bigResult = engine.applyOperator(new ScalarValue(BigInt(2147483647)), "<<", new ScalarValue(BigInt(1)));
const result = engine.applyOperator(new ScalarValue(2147483647), "<<", new ScalarValue(1));
expect(bigResult.value.toString()).toBe("4294967294");
expect(result.value.toString()).toBe("-2");
});
it("can execute all operators without errors", () => {
const operators = [">>", "<<", "|", "&", "^"];
// >>> not supported by BigInt
expect(() => engine.applyOperator(new ScalarValue(1), ">>>", new ScalarValue(2))).not.toThrow();
operators.forEach(o => {
expect(() => engine.applyOperator(new ScalarValue(BigInt(1)), o, new ScalarValue(BigInt(2)))).not.toThrow();
expect(() => engine.applyOperator(new ScalarValue(1), o, new ScalarValue(2))).not.toThrow();
});
});
it('promotes either of operands to BigInt if the other one is', () => {
const bint = new ScalarValue(BigInt(1));
const int = new ScalarValue(1);
const rshift = engine.applyOperator(bint, ">>", int);
expect(rshift.isBigInt()).toBe(true);
expect(rshift.value.toString()).toBe('0');
const lshift = engine.applyOperator(int, "<<", bint);
expect(lshift.isBigInt()).toBe(true);
expect(lshift.value.toString()).toBe('2');
});
});

40
src/expression/engine.ts Normal file
View File

@@ -0,0 +1,40 @@
import { NumberType } from "../core/types";
import ScalarValue from "./ScalarValue";
const engine = {
applyNotOperator(operand: ScalarValue) : ScalarValue {
return new ScalarValue(~operand.value, operand.base);
},
applyOperator(op1 : ScalarValue, operator: string, op2 : ScalarValue) : ScalarValue {
const result = evalute(op1.value, operator, op2.value);
return new ScalarValue(result, op2.base);
}
};
function evalute(op1 : NumberType, operator: string, op2 : NumberType) : NumberType{
const a = equalizeType(op2, op1) as any;
const b = equalizeType(op1, op2) as any;
switch(operator) {
case ">>": return (a >> b) as (NumberType);
case ">>>": return (a >>> b) as (NumberType);
case "<<": return (a << b) as (NumberType);
case "&": return (b & a) as (NumberType);
case "|": return (b | a) as (NumberType);
case "^": return (b ^ a) as (NumberType);
default: throw new Error(operator + " operator is not supported");
}
}
function equalizeType(source : NumberType, dest : NumberType) : NumberType {
return typeof source == 'bigint' && typeof dest != 'bigint'
? BigInt(dest)
: dest;
}
export default engine;

View File

@@ -1,14 +1,14 @@
import { ScalarToken } from "./expression";
import { ScalarValue } from "./expression";
export interface Expression
{
expressionString: string;
}
export interface ExpressionToken
export interface ExpressionElement
{
isOperator: boolean;
getUnderlyingScalarOperand: () => ScalarToken;
evaluate(operand? : ScalarToken): ScalarToken;
getUnderlyingScalarOperand: () => ScalarValue;
evaluate(operand? : ScalarValue): ScalarValue;
}

View File

@@ -1,4 +1,4 @@
import { parser, ListOfNumbersExpression, BitwiseOperationExpression, ScalarToken, OperatorToken } from "./expression";
import { parser, ListOfNumbersExpression, BitwiseOperationExpression, ScalarValue, BitwiseOperator } from "./expression";
describe("expression parser", () => {
@@ -25,8 +25,8 @@ describe("expression parser", () => {
expect(actual).toBeInstanceOf(ListOfNumbersExpression);
const expr = actual as ListOfNumbersExpression;
expect(expr.children[0].getUnderlyingScalarOperand().value).toBe(305419896);
expect(expr.children[1].getUnderlyingScalarOperand().value).toBe(2863311360);
expect(expr.children[0].getUnderlyingScalarOperand().value.toString()).toBe('305419896');
expect(expr.children[1].getUnderlyingScalarOperand().value.toString()).toBe('2863311360');
})
it("pares multiple operand expression", () => {
@@ -36,17 +36,17 @@ describe("expression parser", () => {
const first = result.children[0];
const second = result.children[1];
expect(first).toBeInstanceOf(ScalarToken);
expect(first).toBeInstanceOf(ScalarValue);
expect((first as ScalarToken).value).toBe(1);
expect((first as ScalarValue).value).toBe(1);
expect(second).toBeInstanceOf(OperatorToken);
var secondOp = second as OperatorToken;
expect(second).toBeInstanceOf(BitwiseOperator);
var secondOp = second as BitwiseOperator;
expect(secondOp.operator).toBe("^");
expect(secondOp.operand).toBeInstanceOf(ScalarToken);
var childOp = secondOp.operand as ScalarToken;
expect(childOp.value).toBe(2);
expect(secondOp.operand).toBeInstanceOf(ScalarValue);
var childOp = secondOp.operand as ScalarValue;
expect(childOp.value.toString()).toBe('2');
});
it("bug", () => {

View File

@@ -1,12 +1,11 @@
import ScalarToken from './ScalarToken';
import OperatorToken from './OperatorToken'
import ScalarValue from './ScalarValue';
import BitwiseOperator from './BitwiseOperator'
import ListOfNumbersExpression from './ListOfNumbersExpression';
import BitwiseOperationExpression from './BitwiseOperationExpression';
import { Expression, ExpressionToken } from './expression-interfaces';
import { NumberBase } from '../core/formatter';
import { Expression, ExpressionElement } from './expression-interfaces';
export { default as ScalarToken } from './ScalarToken';
export { default as OperatorToken } from './OperatorToken';
export { default as ScalarValue } from './ScalarValue';
export { default as BitwiseOperator } from './BitwiseOperator';
export { default as ListOfNumbersExpression } from './ListOfNumbersExpression';
export { default as BitwiseOperationExpression } from './BitwiseOperationExpression';
@@ -47,14 +46,6 @@ class ExpressionParser {
return null;
};
parseOperand (input : string) : ScalarToken {
return ScalarToken.parse(input);
};
createOperand (number : number, base : NumberBase) : ScalarToken {
return ScalarToken.create(number, base);
};
addFactory (factory: IExpressionParserFactory) {
this.factories.push(factory);
}
@@ -70,7 +61,7 @@ class ListOfNumbersExpressionFactory implements IExpressionParserFactory
return input.split(' ')
.filter(p => p.length > 0)
.map(p => ScalarToken.tryParse(p))
.map(p => ScalarValue.tryParse(p))
.filter(n => n == null)
.length == 0;
};
@@ -79,7 +70,7 @@ class ListOfNumbersExpressionFactory implements IExpressionParserFactory
const numbers = input.split(' ')
.filter(p => p.length > 0)
.map(m => ScalarToken.parse(m));
.map(m => ScalarValue.parse(m));
return new ListOfNumbersExpression(input, numbers);
}
@@ -90,8 +81,8 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory {
regex: RegExp;
constructor() {
this.fullRegex = /^((<<|>>|>>>|\||\&|\^)?(~?-?([b,x,a-f,0-9]+)))+$/;
this.regex = /(<<|>>|>>>|\||\&|\^)?(~?-?(?:[b,x,a-f,0-9]+))/g;
this.fullRegex = /^((<<|>>|>>>|\||\&|\^)?(~?-?([b,x,l,L,a-f,0-9]+)))+$/;
this.regex = /(<<|>>|>>>|\||\&|\^)?(~?-?(?:[b,x,l,L,a-f,0-9]+))/g;
}
canCreate (input: string) : boolean {
@@ -101,7 +92,7 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory {
create (input: string) : Expression {
var m : RegExpExecArray | null;
const operands : ExpressionToken[] = [];
const operands : ExpressionElement[] = [];
const normalizedString = this.normalizeString(input);
this.regex.lastIndex = 0;
@@ -113,23 +104,23 @@ class BitwiseOperationExpressionFactory implements IExpressionParserFactory {
return new BitwiseOperationExpression(normalizedString, operands)
};
parseMatch (m:any): ExpressionToken {
parseMatch (m:any): ExpressionElement {
var input = m[0],
operator = m[1],
num = m[2];
var parsed = null;
if(num.indexOf('~') == 0) {
parsed = new OperatorToken(ScalarToken.parse(num.substring(1)), '~');
parsed = new BitwiseOperator(ScalarValue.parse(num.substring(1)), '~');
}
else {
parsed = ScalarToken.parse(num);
parsed = ScalarValue.parse(num);
}
if(operator == null) {
return parsed as OperatorToken;
return parsed as BitwiseOperator;
} else {
return new OperatorToken(parsed as ScalarToken, operator);
return new BitwiseOperator(parsed as ScalarValue, operator);
}
};

View File

@@ -0,0 +1,106 @@
import exp from 'constants';
import calc from '../core/calc';
import {numberParser, ParsedNumber} from './numberParser';
describe("parser", () => {
it('parses decimal number', () => {
const result = numberParser.parse('10');
expect(result).not.toBeNull();
var number = result as ParsedNumber;
expect(number.value).toBe(10);
expect(number.base).toBe('dec');
expect(number.input).toBe('10');
});
it('parses bigint numbers', () => {
const dec = numberParser.parse('10L');
expect(dec).not.toBeNull();
expect(dec?.value).toBe(BigInt(10));
expect(typeof dec?.value).toBe("bigint");
expect(dec?.base).toBe('dec');
expect(dec?.input).toBe('10L');
const bin = numberParser.parse('0b10l');
expect(bin).not.toBeNull();
expect(bin?.value).toBe(BigInt(2));
expect(typeof bin?.value).toBe("bigint");
expect(bin?.base).toBe('bin');
expect(bin?.input).toBe('0b10l');
const hex = numberParser.parse('0xfL');
expect(hex).not.toBeNull();
expect(hex?.value.toString()).toBe(BigInt(15).toString());
expect(typeof hex?.value).toBe("bigint");
expect(hex?.base).toBe('hex');
expect(hex?.input).toBe('0xfL');
});
it('switches to bigint if value exceeds max safe int', () => {
const unsafeInt = BigInt(Number.MAX_SAFE_INTEGER)+BigInt(1);
const dec = numberParser.parse(unsafeInt.toString());
expect(dec?.value).toEqual(unsafeInt);
expect(dec?.base).toBe('dec');
const bin = numberParser.parse("0b" + unsafeInt.toString(2));
expect(bin?.value).toEqual(unsafeInt);
expect(bin?.base).toEqual('bin');
const hex = numberParser.parse("0x" + unsafeInt.toString(16));
expect(hex?.value).toEqual(unsafeInt);
expect(hex?.base).toEqual('hex');
});
it('switches to bigint if value exceeds max safe negative int', () => {
const unsafeInt = BigInt(Number.MIN_SAFE_INTEGER)-BigInt(1);
const dec = numberParser.parse(unsafeInt.toString());
expect(dec?.value.toString()).toEqual(unsafeInt.toString());
expect(dec?.base).toBe('dec');
const bin = numberParser.parse("-0b" + unsafeInt.toString(2).replace('-', ''));
expect(bin?.value.toString()).toEqual(unsafeInt.toString());
expect(bin?.base).toEqual('bin');
const hex = numberParser.parse("-0x" + unsafeInt.toString(16).replace('-',''));
expect(hex?.value.toString()).toEqual(unsafeInt.toString());
expect(hex?.base).toEqual('hex');
});
it('parses hex number', () => {
const result = numberParser.parse('0xab');
expect(result).not.toBeNull();
var number = result as ParsedNumber;
expect(number.value).toBe(171);
expect(number.base).toBe('hex');
expect(number.input).toBe('0xab');
});
it('parses bin number', () => {
var result = numberParser.parse('0b0110');
expect(result).not.toBeNull();
var number = result as ParsedNumber;
expect(number.value).toBe(6);
expect(number.base).toBe('bin');
expect(number.input).toBe('0b0110');
});
it('returns null on bad inputs', () => {
expect(numberParser.parse('abc')).toBeNull();
expect(numberParser.parse('')).toBeNull();
});
it('parses big int', () => {
var v = numberParser.parse('1l')?.value
expect(typeof v).toBe("bigint");
expect(v?.toString()).toBe("1");
})
});

View File

@@ -1,38 +0,0 @@
import {numberParser, ParsedNumber} from './numberParser';
describe("parser", () => {
it('parses decimal number', () => {
const result = numberParser.parse('10');
expect(result).not.toBeNull();
var number = result as ParsedNumber;
expect(number.value).toBe(10);
expect(number.base).toBe('dec');
expect(number.input).toBe('10');
});
it('parses hex number', () => {
const result = numberParser.parse('0xab');
expect(result).not.toBeNull();
var number = result as ParsedNumber;
expect(number.value).toBe(171);
expect(number.base).toBe('hex');
expect(number.input).toBe('0xab');
});
it('parses bin number', () => {
var result = numberParser.parse('0b0110');
expect(result).not.toBeNull();
var number = result as ParsedNumber;
expect(number.value).toBe(6);
expect(number.base).toBe('bin');
expect(number.input).toBe('0b0110');
});
it('returns null on bad inputs', () => {
expect(numberParser.parse('abc')).toBeNull();
expect(numberParser.parse('')).toBeNull();
});
});

View File

@@ -1,27 +1,27 @@
import { INT32_MAX_VALUE, INT32_MIN_VALUE } from "../core/const";
import { NumberBase } from "../core/formatter";
import { NumberType } from "../core/types";
const decimalRegex = /^-?\d+$/;
const hexRegex = /^-?0x[0-9,a-f]+$/i;
const binRegex = /^-?0b[0-1]+$/i;
const operatorRegex = /^<<|>>|<<<|\&|\|\^|~$/;
const decimalRegex = /^-?\d+[l,L]?$/;
const hexRegex = /^-?0x[0-9,a-f]+[l,L]?$/i;
const binRegex = /^-?0b[0-1]+[l,L]?$/i;
interface ParserConfig {
regex: RegExp,
radix: number,
base: NumberBase,
prefix: string|RegExp
parse: (input: string) => NumberType
}
export interface ParsedNumber {
value: number;
value: number|bigint;
base: NumberBase;
input: string;
}
var knownParsers : ParserConfig[] = [
{ regex: decimalRegex, radix: 10, base: 'dec', prefix: '^$' },
{ regex: hexRegex, radix: 16, base: 'hex', prefix:/0x/i },
{ regex: binRegex, radix: 2, base: 'bin', prefix:/0b/i }];
{ regex: decimalRegex, base: 'dec', parse:(s) => parseIntSafe(s, 10) },
{ regex: hexRegex, base: 'hex', parse:(s) => parseIntSafe(s, 16)},
{ regex: binRegex, base: 'bin', parse:(s) => parseIntSafe(s, 2) }];
class NumberParser {
@@ -53,7 +53,7 @@ class NumberParser {
return null;
}
var value = parseInt(rawInput.replace(parser.prefix, ''), parser.radix);
var value = parser.parse(rawInput);
return {
value: value,
@@ -63,6 +63,29 @@ class NumberParser {
}
}
const MAX_SAFE_INTn = BigInt(INT32_MAX_VALUE);
const MIN_SAFE_INTn = BigInt(INT32_MIN_VALUE);
function parseIntSafe(input : string, radix: number) : NumberType {
const bigIntStr = input.replace('-', '').replace('l', '').replace('L', '');
let bigInt = BigInt(bigIntStr);
const isNegative = input.startsWith('-');
const isBigInt = input.toLowerCase().endsWith('l');
if(isNegative) bigInt *= BigInt(-1);
if(isBigInt) return bigInt;
if(bigInt > MAX_SAFE_INTn)
return bigInt;
if(bigInt < MIN_SAFE_INTn)
return bigInt;
return parseInt(input.replace(/0(x|b)/, ''), radix);
}
const numberParser = new NumberParser(knownParsers);
export {numberParser};

View File

@@ -32,6 +32,7 @@ code { font-size: 1.2em; font-weight: bold; }
.expression .label { font-weight: bold; padding-right: 5px; text-align: right; }
.expression .bin { letter-spacing: 3px; }
.expression .info { font-size: 0.9em; color: teal; }
.expression .byte { margin: 0 3px; }
.expression-result td { border-top: dotted 1px gray; }
.expression { font-size: 1.5em; font-family: monospace }

View File

@@ -44,7 +44,7 @@ export default class AppState {
this.emphasizeBytes = persistData.emphasizeBytes || true;
this.persistedVersion = persistData.version || 0.1;
this.wasOldVersion = persistData.version != null && this.version > this.persistedVersion;
this.debugMode = env !== 'prod' || persistData.debugMode === true;
this.debugMode = persistData.debugMode === true;
this.pageVisitsCount = persistData.pageVisistsCount || 0;
this.donationClicked = persistData.donationClicked;
}

View File

@@ -28,7 +28,6 @@ export default class AppRoot extends React.Component<AppRootProps, AppRootState>
}
refresh() {
console.log('refresh');
this.setState(this.props.appState);
}

View File

@@ -10,6 +10,7 @@ import HelpResultView from './components/HelpResultView';
import TextResultView from './components/TextResultView';
import WhatsnewResultView from './components/WhatsNewResultView';
import {STARTUP_COMMAND_KEY} from './startup';
import { INT32_MAX_VALUE, INT64_MAX_VALUE } from '../core/const';
const shellModule = {
setup: function(appState: AppState, cmd: CmdShell) {
@@ -31,6 +32,10 @@ const shellModule = {
appState.toggleDebugMode();
appState.addCommandResult(c.input, () => <TextResultView text={`Debug Mode: ${appState.debugMode}`}/>);
});
cmd.command("-max", (c:CommandInput) => {
const text = `Int32 ${INT32_MAX_VALUE}\nInt64 ${INT64_MAX_VALUE}`
appState.addCommandResult(c.input, () => <TextResultView text={text} />)
})
cmd.command("donate", (c:CommandInput) => {