330 lines
8.0 KiB
TypeScript
330 lines
8.0 KiB
TypeScript
import Joi from "joi";
|
|
import { NullOr } from "../../../../utilities";
|
|
import { DomainError, handleDomainError } from "../errors";
|
|
import { RuleValidator } from "../RuleValidator";
|
|
import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject";
|
|
import { Result } from "./Result";
|
|
import { ResultCollection } from "./ResultCollection";
|
|
|
|
const DEFAULT_SCALE = 2;
|
|
|
|
export interface IQuantityOptions extends INullableValueObjectOptions {}
|
|
|
|
export interface IQuantityProps {
|
|
amount: NullOr<number | string>;
|
|
scale?: number;
|
|
}
|
|
|
|
interface IQuantity {
|
|
amount: NullOr<number>;
|
|
scale: number;
|
|
}
|
|
|
|
export interface QuantityObject {
|
|
amount: number | null;
|
|
scale: number;
|
|
}
|
|
|
|
const defaultQuantityProps = {
|
|
amount: 0,
|
|
scale: DEFAULT_SCALE,
|
|
};
|
|
|
|
export class Quantity extends NullableValueObject<IQuantity> {
|
|
public static readonly DEFAULT_SCALE = DEFAULT_SCALE;
|
|
public static readonly MIN_SCALE = 0;
|
|
public static readonly MAX_SCALE = 2;
|
|
|
|
private readonly _isNull: boolean;
|
|
private readonly _options: IQuantityOptions;
|
|
|
|
protected static validate(
|
|
value: NullOr<number | string>,
|
|
scale: NullOr<number>,
|
|
options: IQuantityOptions = {}
|
|
) {
|
|
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED;
|
|
|
|
const ruleEmpty = RuleValidator.RULE_ALLOW_EMPTY;
|
|
|
|
const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label(
|
|
options.label ? options.label : "amount"
|
|
);
|
|
|
|
const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex(/^[-]?\d+$/).label(
|
|
options.label ? options.label : "amount"
|
|
);
|
|
|
|
const ruleScale = Joi.number()
|
|
.min(Quantity.MIN_SCALE)
|
|
.max(Quantity.MAX_SCALE)
|
|
.label(options.label ? options.label : "scale");
|
|
|
|
const validationResults = new ResultCollection([
|
|
RuleValidator.validate<NullOr<number>>(
|
|
Joi.alternatives(ruleNull, ruleEmpty, ruleNumber, ruleString),
|
|
value
|
|
),
|
|
RuleValidator.validate<NullOr<number>>(
|
|
Joi.alternatives(
|
|
RuleValidator.RULE_IS_TYPE_NUMBER.label(options.label ? options.label : "scale"),
|
|
ruleScale
|
|
),
|
|
scale
|
|
),
|
|
]);
|
|
|
|
if (validationResults.hasSomeFaultyResult()) {
|
|
return validationResults.getFirstFaultyResult();
|
|
}
|
|
|
|
return Result.ok();
|
|
}
|
|
|
|
public static create(
|
|
props: IQuantityProps = defaultQuantityProps,
|
|
options: IQuantityOptions = {}
|
|
) {
|
|
if (props === null) {
|
|
throw new Error(`InvalidParams: props params is missing`);
|
|
}
|
|
|
|
const { amount = defaultQuantityProps.amount, scale = defaultQuantityProps.scale } = props;
|
|
|
|
const _options = {
|
|
label: "quantity",
|
|
...options,
|
|
};
|
|
|
|
const validationResult = Quantity.validate(amount, scale, _options);
|
|
|
|
if (validationResult.isFailure) {
|
|
return Result.fail(
|
|
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
|
);
|
|
}
|
|
|
|
let _amount: NullOr<number> = Quantity._sanitize(amount);
|
|
|
|
const _props = {
|
|
amount: _amount === null ? 0 : _amount,
|
|
scale,
|
|
};
|
|
|
|
return Result.ok<Quantity>(new Quantity(_props, _amount === null, options));
|
|
}
|
|
|
|
public static createFromFormattedValue(
|
|
value: NullOr<number | string>,
|
|
_options: IQuantityOptions = {}
|
|
) {
|
|
if (value === null || value === "") {
|
|
return Quantity.create({
|
|
amount: null,
|
|
scale: Quantity.DEFAULT_SCALE,
|
|
});
|
|
}
|
|
|
|
const valueStr = String(value);
|
|
const [integerPart, decimalPart] = valueStr.split(",");
|
|
|
|
let _amount = integerPart;
|
|
let _scale = 2;
|
|
|
|
if (decimalPart === undefined) {
|
|
// 99
|
|
_scale = 0;
|
|
} else {
|
|
if (decimalPart === "") {
|
|
// 99,
|
|
_amount = integerPart + decimalPart.padEnd(1, "0");
|
|
_scale = 1;
|
|
}
|
|
if (decimalPart.length === 1) {
|
|
// 99,1
|
|
_amount = integerPart + decimalPart.padEnd(1, "0");
|
|
_scale = 1;
|
|
} else {
|
|
if (decimalPart.length === 2) {
|
|
// 99,12
|
|
_amount = integerPart + decimalPart.padEnd(2, "0");
|
|
_scale = 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
return Quantity.create(
|
|
{
|
|
amount: _amount,
|
|
scale: _scale,
|
|
},
|
|
_options
|
|
);
|
|
}
|
|
|
|
private static _sanitize(value: NullOr<number | string>): NullOr<number> {
|
|
let _value: NullOr<number> = null;
|
|
|
|
if (typeof value === "string") {
|
|
_value = parseInt(value, 10);
|
|
} else {
|
|
_value = value;
|
|
}
|
|
|
|
return _value;
|
|
}
|
|
|
|
private static _toString(value: NullOr<number>, scale: number): string {
|
|
if (value === null) {
|
|
return "";
|
|
}
|
|
|
|
const factor = Math.pow(10, scale);
|
|
const amount = Number(value) / factor;
|
|
return amount.toFixed(scale);
|
|
}
|
|
|
|
constructor(quantity: IQuantity, isNull: boolean, options: IQuantityOptions) {
|
|
super(quantity);
|
|
this._isNull = Object.freeze(isNull);
|
|
this._options = Object.freeze(options);
|
|
}
|
|
|
|
get amount(): NullOr<number> {
|
|
return this.isNull() ? null : Number(this.props?.amount);
|
|
}
|
|
|
|
get scale(): number {
|
|
return Number(this.props?.scale);
|
|
}
|
|
|
|
public getAmount(): NullOr<number> {
|
|
return this.isNull() ? null : Number(this.props?.amount);
|
|
}
|
|
|
|
public getScale(): number {
|
|
return this.isNull() ? 0 : Number(this.props?.scale);
|
|
}
|
|
|
|
public isEmpty = (): boolean => {
|
|
return this.isNull();
|
|
};
|
|
|
|
public isNull = (): boolean => {
|
|
return this._isNull;
|
|
};
|
|
|
|
public toString(): string {
|
|
return Quantity._toString(this.amount, this.scale);
|
|
}
|
|
|
|
public toNumber(): number {
|
|
return this.isNull() ? 0 : Number(this.toString());
|
|
}
|
|
|
|
public toFormat(): string {
|
|
return this._isNull
|
|
? ""
|
|
: Intl.NumberFormat("es-ES", {
|
|
maximumFractionDigits: 2,
|
|
}).format(this.toNumber());
|
|
}
|
|
|
|
public toPrimitive(): NullOr<number> {
|
|
if (this.scale !== Quantity.DEFAULT_SCALE) {
|
|
return this.convertScale(Quantity.DEFAULT_SCALE).toPrimitive();
|
|
} else {
|
|
return this.amount;
|
|
}
|
|
}
|
|
|
|
public toPrimitives() {
|
|
return this.toObject();
|
|
}
|
|
|
|
public toObject(): QuantityObject {
|
|
return {
|
|
amount: this.amount,
|
|
scale: this.scale,
|
|
};
|
|
}
|
|
|
|
public convertScale(newScale: number): Quantity {
|
|
if (newScale < Quantity.MIN_SCALE || newScale > Quantity.MAX_SCALE) {
|
|
throw new Error(`Scale out of range: ${newScale}`);
|
|
}
|
|
|
|
if (this.isNull()) {
|
|
return new Quantity({ amount: null, scale: newScale }, true, this._options);
|
|
}
|
|
|
|
const oldFactor = Math.pow(10, this.scale);
|
|
const value = Number(this.amount) / oldFactor;
|
|
|
|
const newFactor = Math.pow(10, newScale);
|
|
const newValue = Math.round(value * newFactor);
|
|
|
|
return new Quantity({ amount: newValue, scale: newScale }, false, this._options);
|
|
}
|
|
|
|
public hasSameScale(quantity: Quantity) {
|
|
return this.scale === quantity.scale;
|
|
}
|
|
|
|
public increment(anotherQuantity?: Quantity) {
|
|
if (this.isNull()) {
|
|
return anotherQuantity ? Quantity.create(anotherQuantity.toObject()) : Quantity.create();
|
|
}
|
|
|
|
if (!anotherQuantity) {
|
|
return Quantity.create(
|
|
{
|
|
amount: Number(this.amount) + 1,
|
|
scale: this.scale,
|
|
},
|
|
this._options
|
|
);
|
|
}
|
|
|
|
if (!this.hasSameScale(anotherQuantity)) {
|
|
return Result.fail(Error("No se pueden sumar cantidades con diferentes escalas."));
|
|
}
|
|
|
|
return Quantity.create(
|
|
{
|
|
amount: Number(this.amount) + Number(anotherQuantity.amount),
|
|
scale: this.scale,
|
|
},
|
|
this._options
|
|
);
|
|
}
|
|
|
|
public decrement(anotherQuantity?: Quantity) {
|
|
if (this.isNull()) {
|
|
return anotherQuantity ? Quantity.create(anotherQuantity.toObject()) : Quantity.create();
|
|
}
|
|
|
|
if (!anotherQuantity) {
|
|
return Quantity.create(
|
|
{
|
|
amount: Number(this.amount) - 1,
|
|
scale: this.scale,
|
|
},
|
|
this._options
|
|
);
|
|
}
|
|
|
|
if (!this.hasSameScale(anotherQuantity)) {
|
|
return Result.fail(Error("No se pueden restar cantidades con diferentes escalas."));
|
|
}
|
|
|
|
return Quantity.create(
|
|
{
|
|
amount: Number(this.amount) - Number(anotherQuantity.amount),
|
|
scale: this.scale,
|
|
},
|
|
this._options
|
|
);
|
|
}
|
|
}
|