Skip to content

Commit

Permalink
fix(commerce): return fractional prices (#2458)
Browse files Browse the repository at this point in the history
Co-authored-by: ST-DDT <ST-DDT@gmx.de>
  • Loading branch information
import-brain and ST-DDT committed Mar 1, 2024
1 parent 8e880c1 commit 9348138
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 52 deletions.
15 changes: 15 additions & 0 deletions docs/guide/upgrading_v9/2458.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
### Prices now return more price-like values

The `faker.commerce.price` method now produces values, that also return fractional values.

Old price: 828.00
New price: 828.59

The last digit of the price will adjusted to be more price-like:

- 50% of the time: `9`
- 30% of the time: `5`
- 10% of the time: `0`
- 10% of the time: a random digit from `0` to `9`

We plan to rethink this method some more in the future: [#2579](https://github.com/faker-js/faker/issues/2579)
93 changes: 76 additions & 17 deletions src/modules/commerce/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,23 @@ export class CommerceModule extends ModuleBase {
/**
* Generates a price between min and max (inclusive).
*
* To better represent real-world prices, when `options.dec` is greater than `0`, the final decimal digit in the returned string will be generated as follows:
*
* - 50% of the time: `9`
* - 30% of the time: `5`
* - 10% of the time: `0`
* - 10% of the time: a random digit from `0` to `9`
*
* @param options An options object.
* @param options.min The minimum price. Defaults to `1`.
* @param options.max The maximum price. Defaults to `1000`.
* @param options.dec The number of decimal places. Defaults to `2`.
* @param options.symbol The currency value to use. Defaults to `''`.
*
* @example
* faker.commerce.price() // 828.00
* faker.commerce.price({ min: 100 }) // 904.00
* faker.commerce.price({ min: 100, max: 200 }) // 154.00
* faker.commerce.price() // 828.07
* faker.commerce.price({ min: 100 }) // 904.19
* faker.commerce.price({ min: 100, max: 200 }) // 154.55
* faker.commerce.price({ min: 100, max: 200, dec: 0 }) // 133
* faker.commerce.price({ min: 100, max: 200, dec: 0, symbol: '$' }) // $114
*
Expand Down Expand Up @@ -160,15 +167,22 @@ export class CommerceModule extends ModuleBase {
/**
* Generates a price between min and max (inclusive).
*
* To better represent real-world prices, when `options.dec` is greater than `0`, the final decimal digit in the returned string will be generated as follows:
*
* - 50% of the time: `9`
* - 30% of the time: `5`
* - 10% of the time: `0`
* - 10% of the time: a random digit from `0` to `9`
*
* @param min The minimum price. Defaults to `1`.
* @param max The maximum price. Defaults to `1000`.
* @param dec The number of decimal places. Defaults to `2`.
* @param symbol The currency value to use. Defaults to `''`.
*
* @example
* faker.commerce.price() // 828.00
* faker.commerce.price(100) // 904.00
* faker.commerce.price(100, 200) // 154.00
* faker.commerce.price() // 828.07
* faker.commerce.price(100) // 904.19
* faker.commerce.price(100, 200) // 154.55
* faker.commerce.price(100, 200, 0) // 133
* faker.commerce.price(100, 200, 0, '$') // $114
*
Expand All @@ -180,7 +194,14 @@ export class CommerceModule extends ModuleBase {
/**
* Generates a price between min and max (inclusive).
*
* @param options The minimum price or on options object.
* To better represent real-world prices, when `options.dec` is greater than `0`, the final decimal digit in the returned string will be generated as follows:
*
* - 50% of the time: `9`
* - 30% of the time: `5`
* - 10% of the time: `0`
* - 10% of the time: a random digit from `0` to `9`
*
* @param options The minimum price or an options object.
* @param options.min The minimum price. Defaults to `1`.
* @param options.max The maximum price. Defaults to `1000`.
* @param options.dec The number of decimal places. Defaults to `2`.
Expand All @@ -190,9 +211,9 @@ export class CommerceModule extends ModuleBase {
* @param legacySymbol The currency value to use. This argument is deprecated. Defaults to `''`.
*
* @example
* faker.commerce.price() // 828.00
* faker.commerce.price({ min: 100 }) // 904.00
* faker.commerce.price({ min: 100, max: 200 }) // 154.00
* faker.commerce.price() // 828.07
* faker.commerce.price({ min: 100 }) // 904.19
* faker.commerce.price({ min: 100, max: 200 }) // 154.55
* faker.commerce.price({ min: 100, max: 200, dec: 0 }) // 133
* faker.commerce.price({ min: 100, max: 200, dec: 0, symbol: '$' }) // $114
*
Expand Down Expand Up @@ -234,7 +255,14 @@ export class CommerceModule extends ModuleBase {
/**
* Generates a price between min and max (inclusive).
*
* @param options The minimum price or on options object.
* To better represent real-world prices, when `options.dec` is greater than `0`, the final decimal digit in the returned string will be generated as follows:
*
* - 50% of the time: `9`
* - 30% of the time: `5`
* - 10% of the time: `0`
* - 10% of the time: a random digit from `0` to `9`
*
* @param options The minimum price or an options object.
* @param options.min The minimum price. Defaults to `1`.
* @param options.max The maximum price. Defaults to `1000`.
* @param options.dec The number of decimal places. Defaults to `2`.
Expand All @@ -244,9 +272,9 @@ export class CommerceModule extends ModuleBase {
* @param legacySymbol The currency value to use. This argument is deprecated. Defaults to `''`.
*
* @example
* faker.commerce.price() // 828.00
* faker.commerce.price({ min: 100 }) // 904.00
* faker.commerce.price({ min: 100, max: 200 }) // 154.00
* faker.commerce.price() // 828.07
* faker.commerce.price({ min: 100 }) // 904.19
* faker.commerce.price({ min: 100, max: 200 }) // 154.55
* faker.commerce.price({ min: 100, max: 200, dec: 0 }) // 133
* faker.commerce.price({ min: 100, max: 200, dec: 0, symbol: '$' }) // $114
*
Expand Down Expand Up @@ -286,10 +314,41 @@ export class CommerceModule extends ModuleBase {
return `${symbol}0`;
}

// TODO @Shinigami92 2022-11-24: https://github.com/faker-js/faker/issues/350
const randValue = this.faker.number.int({ min, max });
if (min === max) {
return `${symbol}${min.toFixed(dec)}`;
}

const generated = this.faker.number.float({
min,
max,
fractionDigits: dec,
});

if (dec === 0) {
return `${symbol}${generated.toFixed(dec)}`;
}

const oldLastDigit = (generated * 10 ** dec) % 10;
const newLastDigit = this.faker.helpers.weightedArrayElement([
{ weight: 5, value: 9 },
{ weight: 3, value: 5 },
{ weight: 1, value: 0 },
{
weight: 1,
value: this.faker.number.int({ min: 0, max: 9 }),
},
]);

const fraction = (1 / 10) ** dec;
const oldLastDigitValue = oldLastDigit * fraction;
const newLastDigitValue = newLastDigit * fraction;
const combined = generated - oldLastDigitValue + newLastDigitValue;

if (min <= combined && combined <= max) {
return `${symbol}${combined.toFixed(dec)}`;
}

return symbol + randValue.toFixed(dec);
return `${symbol}${generated.toFixed(dec)}`;
}

/**
Expand Down
72 changes: 39 additions & 33 deletions test/modules/__snapshots__/commerce.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,29 @@ exports[`commerce > 42 > isbn > with variant 10 and space separators 1`] = `"0 9

exports[`commerce > 42 > isbn > with variant 13 1`] = `"978-0-9751108-6-7"`;

exports[`commerce > 42 > price > noArgs 1`] = `"375.00"`;
exports[`commerce > 42 > price > noArgs 1`] = `"375.15"`;

exports[`commerce > 42 > price > with max 1`] = `"375.00"`;
exports[`commerce > 42 > price > with float min and float max option 1`] = `"1.05"`;

exports[`commerce > 42 > price > with max option 1`] = `"501.00"`;
exports[`commerce > 42 > price > with max 1`] = `"375.15"`;

exports[`commerce > 42 > price > with min 1`] = `"406.00"`;
exports[`commerce > 42 > price > with max option 1`] = `"501.35"`;

exports[`commerce > 42 > price > with min and max 1`] = `"69.00"`;
exports[`commerce > 42 > price > with min 1`] = `"405.85"`;

exports[`commerce > 42 > price > with min and max and decimals 1`] = `"69.0000"`;
exports[`commerce > 42 > price > with min and max 1`] = `"68.75"`;

exports[`commerce > 42 > price > with min and max and decimals and symbol 1`] = `"$69.0000"`;
exports[`commerce > 42 > price > with min and max and decimals 1`] = `"68.7275"`;

exports[`commerce > 42 > price > with min and max and decimals and symbol option 1`] = `"$69.0000"`;
exports[`commerce > 42 > price > with min and max and decimals and symbol 1`] = `"$68.7275"`;

exports[`commerce > 42 > price > with min and max and decimals option 1`] = `"69.0000"`;
exports[`commerce > 42 > price > with min and max and decimals and symbol option 1`] = `"$68.7275"`;

exports[`commerce > 42 > price > with min and max option 1`] = `"69.00"`;
exports[`commerce > 42 > price > with min and max and decimals option 1`] = `"68.7275"`;

exports[`commerce > 42 > price > with min option 1`] = `"401.00"`;
exports[`commerce > 42 > price > with min and max option 1`] = `"68.75"`;

exports[`commerce > 42 > price > with min option 1`] = `"400.85"`;

exports[`commerce > 42 > product 1`] = `"Pants"`;

Expand All @@ -56,27 +58,29 @@ exports[`commerce > 1211 > isbn > with variant 10 and space separators 1`] = `"1

exports[`commerce > 1211 > isbn > with variant 13 1`] = `"978-1-82966-736-0"`;

exports[`commerce > 1211 > price > noArgs 1`] = `"929.00"`;
exports[`commerce > 1211 > price > noArgs 1`] = `"928.69"`;

exports[`commerce > 1211 > price > with float min and float max option 1`] = `"1.10"`;

exports[`commerce > 1211 > price > with max 1`] = `"929.00"`;
exports[`commerce > 1211 > price > with max 1`] = `"928.69"`;

exports[`commerce > 1211 > price > with max option 1`] = `"1242.00"`;
exports[`commerce > 1211 > price > with max option 1`] = `"1241.59"`;

exports[`commerce > 1211 > price > with min 1`] = `"933.00"`;
exports[`commerce > 1211 > price > with min 1`] = `"932.19"`;

exports[`commerce > 1211 > price > with min and max 1`] = `"97.00"`;
exports[`commerce > 1211 > price > with min and max 1`] = `"96.49"`;

exports[`commerce > 1211 > price > with min and max and decimals 1`] = `"97.0000"`;
exports[`commerce > 1211 > price > with min and max and decimals 1`] = `"96.4269"`;

exports[`commerce > 1211 > price > with min and max and decimals and symbol 1`] = `"$97.0000"`;
exports[`commerce > 1211 > price > with min and max and decimals and symbol 1`] = `"$96.4269"`;

exports[`commerce > 1211 > price > with min and max and decimals and symbol option 1`] = `"$97.0000"`;
exports[`commerce > 1211 > price > with min and max and decimals and symbol option 1`] = `"$96.4269"`;

exports[`commerce > 1211 > price > with min and max and decimals option 1`] = `"97.0000"`;
exports[`commerce > 1211 > price > with min and max and decimals option 1`] = `"96.4269"`;

exports[`commerce > 1211 > price > with min and max option 1`] = `"97.00"`;
exports[`commerce > 1211 > price > with min and max option 1`] = `"96.49"`;

exports[`commerce > 1211 > price > with min option 1`] = `"932.00"`;
exports[`commerce > 1211 > price > with min option 1`] = `"931.59"`;

exports[`commerce > 1211 > product 1`] = `"Sausages"`;

Expand All @@ -100,27 +104,29 @@ exports[`commerce > 1337 > isbn > with variant 10 and space separators 1`] = `"0

exports[`commerce > 1337 > isbn > with variant 13 1`] = `"978-0-12-435297-1"`;

exports[`commerce > 1337 > price > noArgs 1`] = `"263.00"`;
exports[`commerce > 1337 > price > noArgs 1`] = `"262.79"`;

exports[`commerce > 1337 > price > with float min and float max option 1`] = `"1.09"`;

exports[`commerce > 1337 > price > with max 1`] = `"263.00"`;
exports[`commerce > 1337 > price > with max 1`] = `"262.79"`;

exports[`commerce > 1337 > price > with max option 1`] = `"351.00"`;
exports[`commerce > 1337 > price > with max option 1`] = `"351.09"`;

exports[`commerce > 1337 > price > with min 1`] = `"299.00"`;
exports[`commerce > 1337 > price > with min 1`] = `"298.99"`;

exports[`commerce > 1337 > price > with min and max 1`] = `"63.00"`;
exports[`commerce > 1337 > price > with min and max 1`] = `"63.19"`;

exports[`commerce > 1337 > price > with min and max and decimals 1`] = `"63.0000"`;
exports[`commerce > 1337 > price > with min and max and decimals 1`] = `"63.1019"`;

exports[`commerce > 1337 > price > with min and max and decimals and symbol 1`] = `"$63.0000"`;
exports[`commerce > 1337 > price > with min and max and decimals and symbol 1`] = `"$63.1019"`;

exports[`commerce > 1337 > price > with min and max and decimals and symbol option 1`] = `"$63.0000"`;
exports[`commerce > 1337 > price > with min and max and decimals and symbol option 1`] = `"$63.1019"`;

exports[`commerce > 1337 > price > with min and max and decimals option 1`] = `"63.0000"`;
exports[`commerce > 1337 > price > with min and max and decimals option 1`] = `"63.1019"`;

exports[`commerce > 1337 > price > with min and max option 1`] = `"63.00"`;
exports[`commerce > 1337 > price > with min and max option 1`] = `"63.19"`;

exports[`commerce > 1337 > price > with min option 1`] = `"293.00"`;
exports[`commerce > 1337 > price > with min option 1`] = `"293.09"`;

exports[`commerce > 1337 > product 1`] = `"Ball"`;

Expand Down
33 changes: 31 additions & 2 deletions test/modules/commerce.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('commerce', () => {
.it('with min option', { min: 42 })
.it('with max option', { max: 1337 })
.it('with min and max option', { min: 50, max: 100 })
.it('with float min and float max option', { min: 1, max: 1.1 })
.it('with min and max and decimals option', {
min: 50,
max: 100,
Expand Down Expand Up @@ -124,14 +125,42 @@ describe('commerce', () => {
const price = faker.commerce.price(100, 100, 1);

expect(price).toBeTruthy();
expect(price, 'the price should be equal 100.0').toBe('100.0');
expect(price, 'the price should equal 100.0').toBe('100.0');
});

it('should handle argument dec = 0', () => {
const price = faker.commerce.price(100, 100, 0);

expect(price).toBeTruthy();
expect(price, 'the price should be equal 100').toBe('100');
expect(price, 'the price should equal 100').toBe('100');
});

it('should return decimal values between min and max', () => {
const result = faker.helpers.multiple(
() => faker.commerce.price(1, 1.1, 2),
{ count: 50 }
);

for (const price of result) {
const parsedPrice = Number.parseFloat(price);

expect(parsedPrice).toBeLessThanOrEqual(1.1);
expect(parsedPrice).toBeGreaterThanOrEqual(1);
}
});

it('should return values with three decimal places between min and max', () => {
const result = faker.helpers.multiple(
() => faker.commerce.price({ min: 0.001, max: 0.009, dec: 3 }),
{ count: 50 }
);

for (const price of result) {
const parsedPrice = Number.parseFloat(price);

expect(parsedPrice).toBeLessThanOrEqual(0.009);
expect(parsedPrice).toBeGreaterThanOrEqual(0.001);
}
});
});

Expand Down

0 comments on commit 9348138

Please sign in to comment.