This commit is contained in:
David Arranz 2024-07-15 15:58:12 +02:00
parent c86684812c
commit ab36d6e084
3 changed files with 128 additions and 64 deletions

View File

@ -179,7 +179,7 @@ export class Percentage extends NullableValueObject<IPercentage> {
}; };
public toNumber(): number { public toNumber(): number {
return Percentage._toNumber(this.scale, this.scale); return Percentage._toNumber(this.amount, this.scale);
} }
public toString(): string { public toString(): string {

View File

@ -6,14 +6,14 @@ describe("Quantity Value Object", () => {
const validQuantity = Quantity.create({ amount: 5 }); const validQuantity = Quantity.create({ amount: 5 });
expect(validQuantity.isSuccess).toBe(true); expect(validQuantity.isSuccess).toBe(true);
expect(validQuantity.object.toNumber()).toBe(5); expect(validQuantity.object.toNumber()).toBe(0.05);
}); });
it("Should create a valid quantity (string)", () => { it("Should create a valid quantity (string)", () => {
const validQuantity = Quantity.create({ amount: "99" }); const validQuantity = Quantity.create({ amount: "99" });
expect(validQuantity.isSuccess).toBe(true); expect(validQuantity.isSuccess).toBe(true);
expect(validQuantity.object.toNumber()).toBe(99); expect(validQuantity.object.toNumber()).toBe(0.99);
}); });
it("Should create a valid quantity (null)", () => { it("Should create a valid quantity (null)", () => {
@ -28,21 +28,21 @@ describe("Quantity Value Object", () => {
const nullQuantity = Quantity.create(); const nullQuantity = Quantity.create();
expect(nullQuantity.isSuccess).toBe(true); expect(nullQuantity.isSuccess).toBe(true);
expect(nullQuantity.object.amount).toBe(1); expect(nullQuantity.object.amount).toBe(0);
expect(nullQuantity.object.precision).toBe(0); expect(nullQuantity.object.scale).toBe(2);
}); });
// Prueba la creación de una cantidad válida a partir de una cadena. // Prueba la creación de una cantidad válida a partir de una cadena.
it("Should create a valid quantity from string", () => { it("Should create a valid quantity from string", () => {
const validQuantityFromString = Quantity.create({ amount: "10" }); const validQuantityFromString = Quantity.create({ amount: "318" });
expect(validQuantityFromString.isSuccess).toBe(true); expect(validQuantityFromString.isSuccess).toBe(true);
expect(validQuantityFromString.object.toNumber()).toBe(10); expect(validQuantityFromString.object.toNumber()).toBe(3.18);
}); });
// Prueba la creación de una cantidad válida a partir de una cadena con decimales. // Prueba la creación de una cantidad válida a partir de una cadena con decimales.
it("Should create a valid quantity from string", () => { it("Should create a valid quantity from string", () => {
const validQuantityFromString = Quantity.create({ amount: "123456", precision: 2 }); const validQuantityFromString = Quantity.create({ amount: "123456", scale: 2 });
expect(validQuantityFromString.isSuccess).toBe(true); expect(validQuantityFromString.isSuccess).toBe(true);
expect(validQuantityFromString.object.toNumber()).toBe(1234.56); expect(validQuantityFromString.object.toNumber()).toBe(1234.56);
@ -57,7 +57,7 @@ describe("Quantity Value Object", () => {
// Prueba la conversión a número. // Prueba la conversión a número.
it("Should convert to number", () => { it("Should convert to number", () => {
const quantity = Quantity.create({ amount: 7 }).object; const quantity = Quantity.create({ amount: 700 }).object;
const result = quantity.toNumber(); const result = quantity.toNumber();
expect(result).toBe(7); expect(result).toBe(7);
@ -65,7 +65,7 @@ describe("Quantity Value Object", () => {
// Prueba la conversión a cadena. // Prueba la conversión a cadena.
it("Should convert to string", () => { it("Should convert to string", () => {
const quantity = Quantity.create({ amount: 15 }).object; const quantity = Quantity.create({ amount: 1500 }).object;
const result = quantity.toString(); const result = quantity.toString();
expect(result).toBe("15"); expect(result).toBe("15");
@ -73,10 +73,12 @@ describe("Quantity Value Object", () => {
// Prueba la operación de incremento. // Prueba la operación de incremento.
it("Should increment", () => { it("Should increment", () => {
const quantity = Quantity.create({ amount: 5 }).object; const quantity = Quantity.create({ amount: 5, scale: 2 }).object;
const incrementedQuantity = quantity.increment().object;
expect(incrementedQuantity.toNumber()).toBe(6); const incrementedQuantity = quantity.increment().object;
console.log(quantity.toNumber());
expect(incrementedQuantity.toNumber()).toBe(0.06);
}); });
it("Should increment quantity", () => { it("Should increment quantity", () => {
@ -84,12 +86,12 @@ describe("Quantity Value Object", () => {
const secountQ = Quantity.create({ amount: 105 }).object; const secountQ = Quantity.create({ amount: 105 }).object;
const incrementedQuantity = firstQ.increment(secountQ).object; const incrementedQuantity = firstQ.increment(secountQ).object;
expect(incrementedQuantity.toNumber()).toBe(100); expect(incrementedQuantity.toNumber()).toBe(1.0);
}); });
// Prueba la operación de decremento. // Prueba la operación de decremento.
it("Should decrement", () => { it("Should decrement", () => {
const quantity = Quantity.create({ amount: 0 }).object; const quantity = Quantity.create({ amount: 0, scale: 0 }).object;
const decrementedQuantity = quantity.decrement().object; const decrementedQuantity = quantity.decrement().object;
expect(decrementedQuantity.toNumber()).toBe(-1); expect(decrementedQuantity.toNumber()).toBe(-1);
@ -97,8 +99,10 @@ describe("Quantity Value Object", () => {
// Prueba la operación de decremento. // Prueba la operación de decremento.
it("Should decrement quantity", () => { it("Should decrement quantity", () => {
const quantity = Quantity.create({ amount: 10 }).object; const quantity = Quantity.create({ amount: 100, scale: 1 }).object;
const decrementedQuantity = quantity.decrement(Quantity.create({ amount: 110 }).object).object; const decrementedQuantity = quantity.decrement(
Quantity.create({ amount: 1100, scale: 1 }).object
).object;
expect(decrementedQuantity.toNumber()).toBe(-100); expect(decrementedQuantity.toNumber()).toBe(-100);
}); });

View File

@ -4,36 +4,45 @@ import { NullOr } from "../../../../utilities";
import { RuleValidator } from "../RuleValidator"; import { RuleValidator } from "../RuleValidator";
import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject"; import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject";
import { Result } from "./Result"; import { Result } from "./Result";
import { ResultCollection } from "./ResultCollection";
const DEFAULT_SCALE = 2;
export interface IQuantityOptions extends INullableValueObjectOptions {} export interface IQuantityOptions extends INullableValueObjectOptions {}
export interface IQuantityProps { export interface IQuantityProps {
amount: NullOr<number | string>; amount: NullOr<number | string>;
precision?: number; scale?: number;
} }
interface IQuantity { interface IQuantity {
amount: NullOr<number>; amount: NullOr<number>;
precision: number; scale: number;
} }
export interface QuantityObject { export interface QuantityObject {
amount: number; amount: number;
precision: number; scale: number;
} }
const defaultQuantityProps = { const defaultQuantityProps = {
amount: 0, amount: 0,
precision: 0, scale: DEFAULT_SCALE,
}; };
export class Quantity extends NullableValueObject<IQuantity> { export class Quantity extends NullableValueObject<IQuantity> {
public static readonly DEFAULT_PRECISION = defaultQuantityProps.precision; 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 _isNull: boolean;
private readonly _options: IQuantityOptions; private readonly _options: IQuantityOptions;
protected static validate(value: NullOr<number | string>, options: IQuantityOptions = {}) { protected static validate(
value: NullOr<number | string>,
scale: NullOr<number>,
options: IQuantityOptions = {}
) {
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default( const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(
defaultQuantityProps.amount defaultQuantityProps.amount
); );
@ -46,9 +55,38 @@ export class Quantity extends NullableValueObject<IQuantity> {
options.label ? options.label : "quantity" options.label ? options.label : "quantity"
); );
const ruleScale = Joi.number()
.min(Quantity.MIN_SCALE)
.max(Quantity.MAX_SCALE)
.label(options.label ? options.label : "quantity");
const rules = Joi.alternatives(ruleNull, ruleNumber, ruleString); const rules = Joi.alternatives(ruleNull, ruleNumber, ruleString);
return RuleValidator.validate<NullOr<number>>(rules, value); const validationResults = new ResultCollection([
RuleValidator.validate<NullOr<number>>(
Joi.alternatives(ruleNull, ruleNumber, ruleString),
value
),
RuleValidator.validate<NullOr<number>>(
Joi.alternatives(ruleNull, RuleValidator.RULE_IS_TYPE_NUMBER, ruleScale),
scale
),
]);
if (validationResults.hasSomeFaultyResult()) {
return validationResults.getFirstFaultyResult();
}
// Convert the value to a number if it's a string
let numericValue = typeof value === "string" ? parseInt(value, 10) : Number(value);
// Check if scale is null, and set to default if so
let numericScale = isNull(scale) ? Quantity.DEFAULT_SCALE : Number(scale);
// Calculate the adjusted value
const adjustedValue = numericValue / Math.pow(10, numericScale);
return Result.ok();
} }
public static create( public static create(
@ -59,30 +97,30 @@ export class Quantity extends NullableValueObject<IQuantity> {
throw new Error(`InvalidParams: props params is missing`); throw new Error(`InvalidParams: props params is missing`);
} }
const { amount = defaultQuantityProps.amount, precision = defaultQuantityProps.precision } = const { amount = defaultQuantityProps.amount, scale = defaultQuantityProps.scale } = props;
props;
const _options = { const _options = {
label: "quantity", label: "quantity",
...options, ...options,
}; };
const validationResult = Quantity.validate(amount, _options); const validationResult = Quantity.validate(amount, scale, _options);
if (validationResult.isFailure) { if (validationResult.isFailure) {
return Result.fail(validationResult.error); return Result.fail(validationResult.error);
} }
let _amount: NullOr<number> = Quantity.sanitize(validationResult.object); let _amount: NullOr<number> = Quantity._sanitize(amount);
const _props = { const _props = {
amount: isNull(_amount) ? 0 : _amount, amount: isNull(_amount) ? 0 : _amount,
precision, scale,
}; };
return Result.ok<Quantity>(new this(_props, isNull(_amount), options)); return Result.ok<Quantity>(new this(_props, isNull(_amount), options));
} }
private static sanitize(value: NullOr<number | string>): NullOr<number> { private static _sanitize(value: NullOr<number | string>): NullOr<number> {
let _value: NullOr<number> = null; let _value: NullOr<number> = null;
if (typeof value === "string") { if (typeof value === "string") {
@ -94,6 +132,16 @@ export class Quantity extends NullableValueObject<IQuantity> {
return _value; return _value;
} }
private static _toNumber(value: NullOr<number>, scale: number): number {
if (isNull(value)) {
return 0;
}
const factor = Math.pow(10, scale);
const amount = Number(value) / factor;
return Number(amount.toFixed(scale));
}
constructor(quantity: IQuantity, isNull: boolean, options: IQuantityOptions) { constructor(quantity: IQuantity, isNull: boolean, options: IQuantityOptions) {
super(quantity); super(quantity);
this._isNull = Object.freeze(isNull); this._isNull = Object.freeze(isNull);
@ -104,16 +152,16 @@ export class Quantity extends NullableValueObject<IQuantity> {
return this.isNull() ? null : Number(this.props?.amount); return this.isNull() ? null : Number(this.props?.amount);
} }
get precision(): number { get scale(): number {
return this.isNull() ? 0 : Number(this.props?.precision); return this.isNull() ? 0 : Number(this.props?.scale);
} }
public getAmount(): NullOr<number> { public getAmount(): NullOr<number> {
return this.isNull() ? null : Number(this.props?.amount); return this.isNull() ? null : Number(this.props?.amount);
} }
public getPrecision(): number { public getScale(): number {
return this.isNull() ? 0 : Number(this.props?.precision); return this.isNull() ? 0 : Number(this.props?.scale);
} }
public isEmpty = (): boolean => { public isEmpty = (): boolean => {
@ -125,13 +173,7 @@ export class Quantity extends NullableValueObject<IQuantity> {
}; };
public toNumber(): number { public toNumber(): number {
if (this.isNull()) { return Quantity._toNumber(this.amount, this.scale);
return 0;
}
const factor = Math.pow(10, this.precision);
const amount = Number(this.amount) / factor;
return Number(amount.toFixed(this.precision));
} }
public toString(): string { public toString(): string {
@ -149,65 +191,83 @@ export class Quantity extends NullableValueObject<IQuantity> {
public toObject(): QuantityObject { public toObject(): QuantityObject {
return { return {
amount: this.amount ? this.amount : 0, amount: this.amount ? this.amount : 0,
precision: this.precision, scale: this.scale,
}; };
} }
public hasSamePrecision(quantity: Quantity) { public convertScale(newScale: number): Quantity {
return this.precision === quantity.precision; 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) { public increment(anotherQuantity?: Quantity) {
if (this.isNull()) {
return anotherQuantity ? Quantity.create(anotherQuantity.toObject()) : Quantity.create();
}
if (!anotherQuantity) { if (!anotherQuantity) {
return Quantity.create( return Quantity.create(
{ {
amount: this.toNumber() + 1, amount: Number(this.amount) + 1,
precision: this.precision, scale: this.scale,
}, },
this._options this._options
); );
} }
if (!this.hasSamePrecision(anotherQuantity)) { if (!this.hasSameScale(anotherQuantity)) {
return Result.fail(Error("No se pueden sumar cantidades con diferentes precisiones.")); return Result.fail(Error("No se pueden sumar cantidades con diferentes escalas."));
}
if (this.isNull()) {
return Quantity.create(anotherQuantity.toObject());
} }
return Quantity.create( return Quantity.create(
{ {
amount: this.toNumber() + anotherQuantity.toNumber(), amount: Number(this.amount) + Number(anotherQuantity.amount),
precision: this.precision, scale: this.scale,
}, },
this._options this._options
); );
} }
public decrement(anotherQuantity?: Quantity) { public decrement(anotherQuantity?: Quantity) {
if (this.isNull()) {
return anotherQuantity ? Quantity.create(anotherQuantity.toObject()) : Quantity.create();
}
if (!anotherQuantity) { if (!anotherQuantity) {
return Quantity.create( return Quantity.create(
{ {
amount: this.toNumber() - 1, amount: Number(this.amount) - 1,
precision: this.precision, scale: this.scale,
}, },
this._options this._options
); );
} }
if (!this.hasSamePrecision(anotherQuantity)) { if (!this.hasSameScale(anotherQuantity)) {
return Result.fail(Error("No se pueden restar cantidades con diferentes precisiones.")); return Result.fail(Error("No se pueden restar cantidades con diferentes escalas."));
}
if (this.isNull()) {
return Quantity.create(anotherQuantity.toObject());
} }
return Quantity.create( return Quantity.create(
{ {
amount: this.toNumber() - anotherQuantity.toNumber(), amount: Number(this.amount) - Number(anotherQuantity.amount),
precision: this.precision, scale: this.scale,
}, },
this._options this._options
); );