Skip to content

Commit

Permalink
Merge pull request #3 from bugless/tabular-tests
Browse files Browse the repository at this point in the history
Add more tests and improve parser and toString implementations
  • Loading branch information
gusbicalho authored Jul 16, 2021
2 parents b70d02c + be8845a commit bee976a
Show file tree
Hide file tree
Showing 12 changed files with 525 additions and 238 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
run: dart pub get
- name: Check formatting
if: always() && steps.install.outcome == 'success'
run: dart format --set-exit-if-changed .
run: dart format --line-length 120 --set-exit-if-changed .
- name: Run analyzer and annotate
if: always() && steps.install.outcome == 'success'
uses: kitek/dartanalyzer-annotations-action@v1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/push_main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
run: dart pub get
- name: Check formatting
if: always() && steps.install.outcome == 'success'
run: dart format --set-exit-if-changed .
run: dart format --line-length 120 --set-exit-if-changed .
test:
runs-on: ubuntu-latest
steps:
Expand Down
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"dart.lineLength": 120,
"[dart]": {
"editor.rulers": [
120
],
}
}
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## 0.1.0
- Better parser and toString implementations
- Fix problems with rounding
- More unit tests

## 0.0.1
- Adds some basic operators: `+`, `-`, `*`, `divide`, `abs`, `</<=/>/>=/==`
- CompareTo has a basic implementation that lacks optimization
Expand Down
3 changes: 1 addition & 2 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ analyzer:
implicit-dynamic: false
exclude:
- example/**/*.dart
- test/**/*.dart
linter:
rules:
# - public_member_api_docs
# - type_annotate_public_apis
- void_checks
- unnecessary_new
- unnecessary_new
184 changes: 132 additions & 52 deletions lib/src/big_decimal.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
final _pattern = RegExp(r'^([+-]?\d*)(\.\d*)?([eE][+-]?\d+)?$');

enum RoundingMode {
UP,
DOWN,
Expand All @@ -11,6 +9,14 @@ enum RoundingMode {
UNNECESSARY,
}

const plusCode = 43;
const minusCode = 45;
const dotCode = 46;
const smallECode = 101;
const capitalECode = 69;
const zeroCode = 48;
const nineCode = 57;

class BigDecimal implements Comparable<BigDecimal> {
BigDecimal._({
required this.intVal,
Expand All @@ -24,46 +30,96 @@ class BigDecimal implements Comparable<BigDecimal> {
);
}

static int nextNonDigit(String value, [int start = 0]) {
var index = start;
for (; index < value.length; index++) {
final code = value.codeUnitAt(index);
if (code < zeroCode || code > nineCode) {
break;
}
}
return index;
}

static BigDecimal? tryParse(String value) {
try {
return BigDecimal.parse(value);
} catch (e) {
return null;
}
}

factory BigDecimal.parse(String value) {
// TODO: Change this into Java implementation
final match = _pattern.firstMatch(value);
if (match == null) {
throw FormatException('Invalid BigDecimal forma for $value');
var sign = '';
var index = 0;
var nextIndex = 0;

switch (value.codeUnitAt(index)) {
case minusCode:
sign = '-';
index++;
break;
case plusCode:
index++;
break;
default:
break;
}
final intPart = match.group(1)!;
final decimalWithSeparator = match.group(2);

if (decimalWithSeparator != null) {
final decimalPart = decimalWithSeparator.substring(1);
return BigDecimal._(
intVal: BigInt.parse(intPart + decimalPart),
scale: decimalPart.length,
);

nextIndex = nextNonDigit(value, index);
final integerPart = '$sign${value.substring(index, nextIndex)}';
index = nextIndex;

if (index >= value.length) {
return BigDecimal.fromBigInt(BigInt.parse(integerPart));
}

return BigDecimal._(
intVal: BigInt.parse(intPart),
scale: 0,
var decimalPart = '';
if (value.codeUnitAt(index) == dotCode) {
index++;
nextIndex = nextNonDigit(value, index);
decimalPart = value.substring(index, nextIndex);
index = nextIndex;

if (index >= value.length) {
return BigDecimal._(
intVal: BigInt.parse('$integerPart$decimalPart'),
scale: decimalPart.length,
);
}
}

switch (value.codeUnitAt(index)) {
case smallECode:
case capitalECode:
index++;
final exponent = int.parse(value.substring(index));
return BigDecimal._(
intVal: BigInt.parse('$integerPart$decimalPart'),
scale: decimalPart.length - exponent,
);
}

throw Exception(
'Not a valid BigDecimal string representation: $value.\n'
'Unexpected ${value.substring(index)}.',
);
}

final BigInt intVal;
late final int precision = _calculatePrecision();
final int scale;

// TODO: Fix
@override
bool operator ==(dynamic other) =>
other is BigDecimal && compareTo(other) == 0;
bool operator ==(dynamic other) => other is BigDecimal && compareTo(other) == 0;

bool exactlyEquals(dynamic other) => other is BigDecimal && intVal == other.intVal && scale == other.scale;

BigDecimal operator +(BigDecimal other) =>
_add(intVal, other.intVal, scale, other.scale);
BigDecimal operator +(BigDecimal other) => _add(intVal, other.intVal, scale, other.scale);

BigDecimal operator *(BigDecimal other) =>
BigDecimal._(intVal: intVal * other.intVal, scale: scale + other.scale);
BigDecimal operator *(BigDecimal other) => BigDecimal._(intVal: intVal * other.intVal, scale: scale + other.scale);

BigDecimal operator -(BigDecimal other) =>
_add(intVal, -other.intVal, scale, other.scale);
BigDecimal operator -(BigDecimal other) => _add(intVal, -other.intVal, scale, other.scale);

bool operator <(BigDecimal other) => compareTo(other) < 0;

Expand All @@ -82,17 +138,15 @@ class BigDecimal implements Comparable<BigDecimal> {
RoundingMode roundingMode = RoundingMode.UNNECESSARY,
int? scale,
}) =>
_divide(intVal, this.scale, divisor.intVal, divisor.scale,
scale ?? this.scale, roundingMode);
_divide(intVal, this.scale, divisor.intVal, divisor.scale, scale ?? this.scale, roundingMode);

BigDecimal pow(int n) {
if (n >= 0 && n <= 999999999) {
// TODO: Check scale of this multiplication
final newScale = scale * n;
return BigDecimal._(intVal: intVal.pow(n), scale: newScale);
}
throw Exception(
'Invalid operation: Exponent should be between 0 and 999999999');
throw Exception('Invalid operation: Exponent should be between 0 and 999999999');
}

BigDecimal withScale(
Expand All @@ -110,8 +164,7 @@ class BigDecimal implements Comparable<BigDecimal> {
return BigDecimal._(intVal: intResult, scale: newScale);
} else {
final drop = sumScale(scale, -newScale);
return _divideAndRound(intVal, BigInt.from(10).pow(drop), newScale,
roundingMode, newScale);
return _divideAndRound(intVal, BigInt.from(10).pow(drop), newScale, roundingMode, newScale);
}
}
}
Expand All @@ -124,8 +177,7 @@ class BigDecimal implements Comparable<BigDecimal> {
return intVal.abs().compareTo(BigInt.from(10).pow(r)) < 0 ? r : r + 1;
}

static BigDecimal _add(
BigInt intValA, BigInt intValB, int scaleA, int scaleB) {
static BigDecimal _add(BigInt intValA, BigInt intValB, int scaleA, int scaleB) {
final scaleDiff = scaleA - scaleB;
if (scaleDiff == 0) {
return BigDecimal._(intVal: intValA + intValB, scale: scaleA);
Expand Down Expand Up @@ -153,14 +205,12 @@ class BigDecimal implements Comparable<BigDecimal> {
final newScale = scale + divisorScale;
final raise = newScale - dividendScale;
final scaledDividend = dividend * BigInt.from(10).pow(raise);
return _divideAndRound(
scaledDividend, divisor, scale, roundingMode, scale);
return _divideAndRound(scaledDividend, divisor, scale, roundingMode, scale);
} else {
final newScale = sumScale(dividendScale, -scale);
final raise = newScale - divisorScale;
final scaledDivisor = divisor * BigInt.from(10).pow(raise);
return _divideAndRound(
dividend, scaledDivisor, scale, roundingMode, scale);
return _divideAndRound(dividend, scaledDivisor, scale, roundingMode, scale);
}
}

Expand All @@ -172,14 +222,11 @@ class BigDecimal implements Comparable<BigDecimal> {
int preferredScale,
) {
final quotient = dividend ~/ divisor;
final remainder = dividend.remainder(divisor);
final remainder = dividend.remainder(divisor).abs();
final quotientPositive = dividend.sign == divisor.sign;

if (remainder != BigInt.zero) {
if (_needIncrement(
divisor, roundingMode, quotientPositive, quotient, remainder)) {
final intResult =
quotient + (quotientPositive ? BigInt.one : -BigInt.one);
if (_needIncrement(divisor, roundingMode, quotientPositive, quotient, remainder)) {
final intResult = quotient + (quotientPositive ? BigInt.one : -BigInt.one);
return BigDecimal._(intVal: intResult, scale: scale);
}
return BigDecimal._(intVal: quotient, scale: scale);
Expand Down Expand Up @@ -224,8 +271,7 @@ class BigDecimal implements Comparable<BigDecimal> {
BigInt quotient,
BigInt remainder,
) {
final remainderComparisonToHalfDivisor =
(remainder * BigInt.from(2)).compareTo(divisor);
final remainderComparisonToHalfDivisor = (remainder * BigInt.from(2)).compareTo(divisor);
switch (roundingMode) {
case RoundingMode.UNNECESSARY:
throw Exception('Rounding necessary');
Expand Down Expand Up @@ -285,12 +331,46 @@ class BigDecimal implements Comparable<BigDecimal> {
return _add(intVal, -other.intVal, scale, other.scale).intVal.sign;
}

// TODO: Better impl
@override
String toString() {
final intStr = intVal.toString();
return '${intStr.substring(0, intStr.length - scale)}.${intStr.substring(intStr.length - scale)}';
if (scale == 0) {
return intVal.toString();
}

final intStr = intVal.abs().toString();
final b = StringBuffer(intVal.isNegative ? '-' : '');

final adjusted = (intStr.length - 1) - scale;
// Java's heuristic to avoid too many decimal places
if (scale >= 0 && adjusted >= -6) {
print(intStr);
print(scale);
if (intStr.length > scale) {
final integerPart = intStr.substring(0, intStr.length - scale);
b.write(integerPart);

final decimalPart = intStr.substring(intStr.length - scale);
if (decimalPart.isNotEmpty) {
b.write('.$decimalPart');
}
} else {
b..write('0.')..write(intStr.padLeft(scale, '0'));
}
} else {
// Exponential notation
b.write(intStr[0]);
if (intStr.length > 1) {
b..write('.')..write(intStr.substring(1));
}
if (adjusted != 0) {
b.write('e');
if (adjusted > 0) {
b.write('+');
}
b.write(adjusted);
}
}

return b.toString();
}
}

BigDecimal dec(String value) => BigDecimal.parse(value);
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: big_decimal
version: 0.0.1
version: 0.1.0
description: >
A bugless implementation of BigDecimal in Dart based on Java's BigDecimal
Expand Down
Loading

0 comments on commit bee976a

Please sign in to comment.