Skip to content
Merged
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
20 changes: 17 additions & 3 deletions lib/utils/formatListing.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ const ROOMS_LABEL = {
tr: 'Odalar',
};

/**
* German digit grouping for every price, whatever the UI language: the amount is always in euros,
* and "1200000 €" is hard to read at a glance in a push notification.
*/
const PRICE_FORMAT = new Intl.NumberFormat('de-DE', { maximumFractionDigits: 2 });

/**
* Formats a price in euros with thousands separators, e.g. `1000000` becomes `1.000.000 €`.
*
* @param {number} price
* @returns {string}
*/
export const formatPrice = (price) => `${PRICE_FORMAT.format(price)} €`;

/**
* Formats a listing's numerical fields (price, size, rooms) into strings with their respective units.
*
Expand All @@ -41,7 +55,7 @@ export const formatListing = (listing, language = 'en') => {
const rooms = ROOMS_LABEL[language] ?? ROOMS_LABEL.en;
return {
...listing,
price: listing.price != null ? `${listing.price} €` : null,
price: listing.price != null ? formatPrice(listing.price) : null,
size: listing.size != null ? `${listing.size} m²` : null,
rooms: listing.rooms != null ? `${listing.rooms} ${rooms}` : null,
// Null unless a router actually answered for this listing. Adapters drop it with the same
Expand Down Expand Up @@ -101,8 +115,8 @@ export const formatPriceChange = (change, language = 'en') => {
const percent = Math.abs(change.changePercent).toFixed(1);
return {
...formatListing(change.listing, language),
oldPrice: `${change.oldPrice} €`,
newPrice: `${change.newPrice} €`,
oldPrice: formatPrice(change.oldPrice),
newPrice: formatPrice(change.newPrice),
// Signed and with the unit, so an adapter can drop it straight into a line of text.
changePercent: `${change.changePercent < 0 ? '-' : '+'}${percent} %`,
direction: change.direction,
Expand Down
4 changes: 2 additions & 2 deletions test/services/listings/priceHistoryService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ describe('services/listings/priceHistoryService', () => {
await notifyPriceChanges([change('job1')]);

expect(state.sent[0][1][0]).toMatchObject({
oldPrice: '1200 €',
newPrice: '1100 €',
oldPrice: '1.200 €',
newPrice: '1.100 €',
changePercent: '-8.3 %',
changeHeadline: 'Price reduced',
});
Expand Down
45 changes: 43 additions & 2 deletions test/utils/formatListing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { describe, it, expect } from 'vitest';
import { formatListing } from '../../lib/utils/formatListing.js';
import { formatListing, formatPrice, formatPriceChange } from '../../lib/utils/formatListing.js';

/**
* Notification text used to be German whatever the user had set the interface to, because the
Expand All @@ -29,7 +29,7 @@ describe('formatListing', () => {

it('formats price and size with their language-neutral units', () => {
const formatted = formatListing(listing, 'de');
expect(formatted.price).toBe('1200 €');
expect(formatted.price).toBe('1.200 €');
expect(formatted.size).toBe('74 m²');
});

Expand All @@ -54,3 +54,44 @@ describe('formatListing', () => {
expect(listing).toEqual(original);
});
});

describe('formatPrice', () => {
it('groups thousands with dots so large prices stay readable', () => {
expect(formatPrice(1000000)).toBe('1.000.000 €');
expect(formatPrice(100000)).toBe('100.000 €');
expect(formatPrice(1200)).toBe('1.200 €');
});

it('leaves small prices alone', () => {
expect(formatPrice(800)).toBe('800 €');
expect(formatPrice(0)).toBe('0 €');
});

it('uses a decimal comma and keeps at most two decimals', () => {
expect(formatPrice(1234.5)).toBe('1.234,5 €');
expect(formatPrice(999.999)).toBe('1.000 €');
});

it('uses German grouping even when the interface is English', () => {
expect(formatListing({ id: 'l3', title: 'House', price: 450000 }, 'en').price).toBe('450.000 €');
});
});

describe('formatPriceChange', () => {
it('formats old and new price the same way as a new listing', () => {
const formatted = formatPriceChange(
{
listing: { id: 'l4', title: 'House', price: 475000 },
oldPrice: 499000,
newPrice: 475000,
changePercent: -4.81,
direction: 'down',
},
'de',
);
expect(formatted.oldPrice).toBe('499.000 €');
expect(formatted.newPrice).toBe('475.000 €');
expect(formatted.price).toBe('475.000 €');
expect(formatted.changePercent).toBe('-4.8 %');
});
});
Loading