Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## 0.6.0
- Fix static analysis issues
- Add support for Dart 3
- Remove internal members that were exposed to the API

## 0.5.0
- Add toPlainString()

Expand Down
4 changes: 4 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ include: package:lints/recommended.yaml
analyzer:
exclude:
- example/**/*.dart

linter:
rules:
- public_member_api_docs
5 changes: 5 additions & 0 deletions example/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'package:big_decimal/big_decimal.dart';

final decimal1 = BigDecimal.parse('1');
final decimal2 = BigDecimal.parse('2');
final decimal3 = decimal1 + decimal2; // 3
133 changes: 100 additions & 33 deletions lib/src/big_decimal.dart
Original file line number Diff line number Diff line change
@@ -1,52 +1,79 @@
// enum is using screaming snake case due to a direct migration from java
// ignore_for_file: constant_identifier_names

/// rounding mode used when doing operations on a [BigDecimal]
enum RoundingMode {
/// away from zero
UP,

/// towards zero
DOWN,

/// towards +infinity
CEILING,

/// towards -infinity
FLOOR,

/// away from zero if remainder comparison to half divisor is even
HALF_UP,

/// towards zero if remainder comparison to half divisor is even
HALF_DOWN,

/// towards zero if remainder comparison to half divisor is even and [BigDecimal] is odd
HALF_EVEN,

/// does not round at all.
///
/// Throws [Exception] if not exact and needs rounding
UNNECESSARY,
}

const plusCode = 43;
const minusCode = 45;
const dotCode = 46;
const smallECode = 101;
const capitalECode = 69;
const zeroCode = 48;
const nineCode = 57;
const _plusCode = 43;
const _minusCode = 45;
const _dotCode = 46;
const _smallECode = 101;
const _capitalECode = 69;
const _zeroCode = 48;
const _nineCode = 57;

/// representation of an arbitrarily large decimal number
class BigDecimal implements Comparable<BigDecimal> {
BigDecimal._({
required this.intVal,
required this.scale,
});

/// factory constructor of a [BigDecimal] from a [BigInt]
factory BigDecimal.fromBigInt(BigInt value) {
return BigDecimal._(
intVal: value,
scale: 0,
);
}

static BigDecimal zero = BigDecimal.fromBigInt(BigInt.zero);
static BigDecimal one = BigDecimal.fromBigInt(BigInt.one);
static BigDecimal two = BigDecimal.fromBigInt(BigInt.two);
/// a [BigDecimal] with a numerical value of 0
static final zero = BigDecimal.fromBigInt(BigInt.zero);

/// a [BigDecimal] with a numerical value of 1
static final one = BigDecimal.fromBigInt(BigInt.one);

/// a [BigDecimal] with a numerical value of 2
static final two = BigDecimal.fromBigInt(BigInt.two);

static int nextNonDigit(String value, [int start = 0]) {
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) {
if (code < _zeroCode || code > _nineCode) {
break;
}
}
return index;
}

/// try to create a [BigDecimal] from a [String]. Returns null if invalid
static BigDecimal? tryParse(String value) {
try {
return BigDecimal.parse(value);
Expand All @@ -55,24 +82,27 @@ class BigDecimal implements Comparable<BigDecimal> {
}
}

/// try to create a [BigDecimal] from a [String].
///
/// Throws [Exception] if invalid
factory BigDecimal.parse(String value) {
var sign = '';
var index = 0;
var nextIndex = 0;

switch (value.codeUnitAt(index)) {
case minusCode:
case _minusCode:
sign = '-';
index++;
break;
case plusCode:
case _plusCode:
index++;
break;
default:
break;
}

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

Expand All @@ -81,9 +111,9 @@ class BigDecimal implements Comparable<BigDecimal> {
}

var decimalPart = '';
if (value.codeUnitAt(index) == dotCode) {
if (value.codeUnitAt(index) == _dotCode) {
index++;
nextIndex = nextNonDigit(value, index);
nextIndex = _nextNonDigit(value, index);
decimalPart = value.substring(index, nextIndex);
index = nextIndex;

Expand All @@ -96,8 +126,8 @@ class BigDecimal implements Comparable<BigDecimal> {
}

switch (value.codeUnitAt(index)) {
case smallECode:
case capitalECode:
case _smallECode:
case _capitalECode:
index++;
final exponent = int.parse(value.substring(index));
return BigDecimal._(
Expand All @@ -112,38 +142,57 @@ class BigDecimal implements Comparable<BigDecimal> {
);
}

/// the arbitrarily large numeric value without scale
final BigInt intVal;

/// precision of the decimal digits of this
late final int precision = _calculatePrecision();

/// scale of this [BigDecimal]
final int scale;

@override
bool operator ==(dynamic other) =>
bool operator ==(Object other) =>
other is BigDecimal && compareTo(other) == 0;

bool exactlyEquals(dynamic other) =>
/// compares this with [other] for both value and scale
bool exactlyEquals(Object? other) =>
other is BigDecimal && intVal == other.intVal && scale == other.scale;

/// adds this to [other]
BigDecimal operator +(BigDecimal other) =>
_add(intVal, other.intVal, scale, other.scale);

/// multiply this with [other]
BigDecimal operator *(BigDecimal other) =>
BigDecimal._(intVal: intVal * other.intVal, scale: scale + other.scale);

/// subtracts this to [other]
BigDecimal operator -(BigDecimal other) =>
_add(intVal, -other.intVal, scale, other.scale);

/// Whether this is less than [other].
bool operator <(BigDecimal other) => compareTo(other) < 0;

/// Whether this is less than or equal to [other].
bool operator <=(BigDecimal other) => compareTo(other) <= 0;

/// Whether this is greater than [other].
bool operator >(BigDecimal other) => compareTo(other) > 0;

/// Whether this is greater than or equal to [other].
bool operator >=(BigDecimal other) => compareTo(other) >= 0;

/// Negates this big decimal
BigDecimal operator -() => BigDecimal._(intVal: -intVal, scale: scale);

/// Returns the absolute value of this
BigDecimal abs() => BigDecimal._(intVal: intVal.abs(), scale: scale);

/// divides this number by [divisor]. Defaults to not rounding the number.
///
/// Throws [Exception] if rounding is [RoundingMode.UNNECESSARY] but rounding
/// is actually necessary.
BigDecimal divide(
BigDecimal divisor, {
RoundingMode roundingMode = RoundingMode.UNNECESSARY,
Expand All @@ -152,6 +201,7 @@ class BigDecimal implements Comparable<BigDecimal> {
_divide(intVal, this.scale, divisor.intVal, divisor.scale,
scale ?? this.scale, roundingMode);

/// this to the power of [n]
BigDecimal pow(int n) {
if (n >= 0 && n <= 999999999) {
// TODO: Check scale of this multiplication
Expand All @@ -162,13 +212,29 @@ class BigDecimal implements Comparable<BigDecimal> {
'Invalid operation: Exponent should be between 0 and 999999999');
}

/// returns this as a [double]
double toDouble() =>
intVal.toDouble() / BigInt.from(10).pow(scale).toDouble();

/// returns this as a [BigInt] with the desired [roundingMode]
///
/// Throws [Exception] if rounding is [RoundingMode.UNNECESSARY] but rounding
/// is actually necessary.
BigInt toBigInt({RoundingMode roundingMode = RoundingMode.UNNECESSARY}) =>
withScale(0, roundingMode: roundingMode).intVal;

/// returns this as a [int] with the desired [roundingMode]
///
/// Throws [Exception] if rounding is [RoundingMode.UNNECESSARY] but rounding
/// is actually necessary.
int toInt({RoundingMode roundingMode = RoundingMode.UNNECESSARY}) =>
toBigInt(roundingMode: roundingMode).toInt();

/// returns a new [BigDecimal] with the desired [newScale]. May round by
/// [roundingMode].
///
/// Throws [Exception] if rounding is [RoundingMode.UNNECESSARY] but rounding
/// is actually necessary.
BigDecimal withScale(
int newScale, {
RoundingMode roundingMode = RoundingMode.UNNECESSARY,
Expand All @@ -179,11 +245,11 @@ class BigDecimal implements Comparable<BigDecimal> {
return BigDecimal._(intVal: BigInt.zero, scale: newScale);
} else {
if (newScale > scale) {
final drop = sumScale(newScale, -scale);
final drop = _sumScale(newScale, -scale);
final intResult = intVal * BigInt.from(10).pow(drop);
return BigDecimal._(intVal: intResult, scale: newScale);
} else {
final drop = sumScale(scale, -newScale);
final drop = _sumScale(scale, -newScale);
return _divideAndRound(intVal, BigInt.from(10).pow(drop), newScale,
roundingMode, newScale);
}
Expand Down Expand Up @@ -223,14 +289,14 @@ class BigDecimal implements Comparable<BigDecimal> {
if (dividend == BigInt.zero) {
return BigDecimal._(intVal: BigInt.zero, scale: scale);
}
if (sumScale(scale, divisorScale) > dividendScale) {
if (_sumScale(scale, divisorScale) > dividendScale) {
final newScale = scale + divisorScale;
final raise = newScale - dividendScale;
final scaledDividend = dividend * BigInt.from(10).pow(raise);
return _divideAndRound(
scaledDividend, divisor, scale, roundingMode, scale);
} else {
final newScale = sumScale(dividendScale, -scale);
final newScale = _sumScale(dividendScale, -scale);
final raise = newScale - divisorScale;
final scaledDivisor = divisor * BigInt.from(10).pow(raise);
return _divideAndRound(
Expand Down Expand Up @@ -258,14 +324,14 @@ class BigDecimal implements Comparable<BigDecimal> {
return BigDecimal._(intVal: quotient, scale: scale);
} else {
if (preferredScale != scale) {
return createAndStripZerosForScale(quotient, scale, preferredScale);
return _createAndStripZerosForScale(quotient, scale, preferredScale);
} else {
return BigDecimal._(intVal: quotient, scale: scale);
}
}
}

static BigDecimal createAndStripZerosForScale(
static BigDecimal _createAndStripZerosForScale(
BigInt intVal,
int scale,
int preferredScale,
Expand All @@ -284,7 +350,7 @@ class BigDecimal implements Comparable<BigDecimal> {
break;
}
intValMut = intValMut ~/ ten;
scaleMut = sumScale(scaleMut, -1);
scaleMut = _sumScale(scaleMut, -1);
}

return BigDecimal._(intVal: intValMut, scale: scaleMut);
Expand Down Expand Up @@ -334,11 +400,6 @@ class BigDecimal implements Comparable<BigDecimal> {
}
}

static int sumScale(int scaleA, int scaleB) {
// TODO: We need to check for overflows here
return scaleA + scaleB;
}

@override
int compareTo(BigDecimal other) {
if (scale == other.scale) {
Expand Down Expand Up @@ -394,6 +455,7 @@ class BigDecimal implements Comparable<BigDecimal> {
return b.toString();
}

/// returns its [String] represantation without using exponential notation
String toPlainString() {
if (scale == 0) {
return intVal.toString();
Expand Down Expand Up @@ -423,3 +485,8 @@ class BigDecimal implements Comparable<BigDecimal> {
return b.toString();
}
}

int _sumScale(int scaleA, int scaleB) {
// TODO: We need to check for overflows here
return scaleA + scaleB;
}
6 changes: 3 additions & 3 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: big_decimal
version: 0.5.0
version: 0.6.0
description: >
A bugless implementation of BigDecimal in Dart based on Java's BigDecimal

repository: https://github.com/bugless/big-decimal
environment:
sdk: ">=2.12.0 <3.0.0"
sdk: ">=2.12.0 <4.0.0"

dev_dependencies:
lints: ^2.0.1
lints: ^5.1.1
test: ^1.17.2
12 changes: 6 additions & 6 deletions test/big_decimal_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,9 @@ void main() {
String down,
String ceiling,
String floor,
String half_up,
String half_down,
String half_even,
String halfUp,
String halfDown,
String halfEven,
Object unnecessary,
) {
BigDecimal round(RoundingMode mode) =>
Expand All @@ -300,11 +300,11 @@ void main() {
reason: 'CEILING');
expect(round(RoundingMode.FLOOR), exactly(floor.dec),
reason: 'FLOOR');
expect(round(RoundingMode.HALF_UP), exactly(half_up.dec),
expect(round(RoundingMode.HALF_UP), exactly(halfUp.dec),
reason: 'HALF_UP');
expect(round(RoundingMode.HALF_DOWN), exactly(half_down.dec),
expect(round(RoundingMode.HALF_DOWN), exactly(halfDown.dec),
reason: 'HALF_DOWN');
expect(round(RoundingMode.HALF_EVEN), exactly(half_even.dec),
expect(round(RoundingMode.HALF_EVEN), exactly(halfEven.dec),
reason: 'HALF_EVEN');
if (unnecessary is String) {
expect(round(RoundingMode.UNNECESSARY), exactly(unnecessary.dec),
Expand Down