Skip to content

Commit c6187dc

Browse files
committed
pool: display lease durations as human readable times
1 parent 7c26d04 commit c6187dc

File tree

9 files changed

+162
-19
lines changed

9 files changed

+162
-19
lines changed

app/src/__tests__/components/pool/BatchSection.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ describe('BatchSection', () => {
3838
it('should toggle between markets', async () => {
3939
const { getByText, findByText } = render();
4040
expect(store.batchStore.selectedLeaseDuration).toBe(2016);
41-
expect(await findByText('4032')).toBeInTheDocument();
42-
fireEvent.click(getByText('4032'));
41+
expect(await findByText('1 month')).toBeInTheDocument();
42+
fireEvent.click(getByText('1 month'));
4343
expect(store.batchStore.selectedLeaseDuration).toBe(4032);
4444
});
4545
});

app/src/__tests__/components/pool/OrderFormSection.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ describe('OrderFormSection', () => {
109109
changeInput('Bid Premium', '10000');
110110
changeInput('Minimum Channel Size', '100000');
111111
changeInput('Max Batch Fee Rate', '1');
112-
await changeSelect('Channel Duration', '4032 (open)');
112+
await changeSelect('Channel Duration', '1 month (open)');
113113
await changeSelect('Min Node Tier', 'T0 - All Nodes');
114114

115115
let bid: Required<POOL.Bid.AsObject>;

app/src/__tests__/util/formatters.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Big from 'big.js';
22
import { Unit } from 'util/constants';
3-
import { formatSats, formatTime, formatUnit } from 'util/formatters';
3+
import { blocksToTime, formatSats, formatTime, formatUnit } from 'util/formatters';
44

55
describe('formatters Util', () => {
66
describe('formatSats', () => {
@@ -89,4 +89,18 @@ describe('formatters Util', () => {
8989
expect(formatTime(60 * 70 + 1)).toEqual('1h 10m 1s');
9090
});
9191
});
92+
93+
describe('blocksToTime', () => {
94+
it('should convert block to time', () => {
95+
expect(blocksToTime(432)).toEqual('3 days');
96+
expect(blocksToTime(1008)).toEqual('1 week');
97+
expect(blocksToTime(2016)).toEqual('2 weeks');
98+
expect(blocksToTime(4032)).toEqual('1 month');
99+
expect(blocksToTime(8064)).toEqual('2 months');
100+
expect(blocksToTime(12096)).toEqual('3 months');
101+
expect(blocksToTime(16128)).toEqual('4 months');
102+
expect(blocksToTime(24192)).toEqual('6 months');
103+
expect(blocksToTime(52416)).toEqual('1 year');
104+
});
105+
});
92106
});

app/src/components/common/BadgeList.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useCallback } from 'react';
22
import styled from '@emotion/styled';
33
import { Badge } from 'components/base';
4+
import Tip from './Tip';
45

56
const Styled = {
67
Wrapper: styled.div<{ flex?: boolean }>`
@@ -30,6 +31,7 @@ const Styled = {
3031
export interface BadgeListOption {
3132
label: string;
3233
value: string;
34+
tip?: string;
3335
}
3436

3537
interface Props {
@@ -45,9 +47,11 @@ const BadgeList: React.FC<Props> = ({ options, value, onChange }) => {
4547
return (
4648
<Wrapper>
4749
{options.map(o => (
48-
<Badge key={o.value} selected={o.value === value} onClick={handleClick(o.value)}>
49-
{o.label}
50-
</Badge>
50+
<Tip key={o.value} overlay={o.tip}>
51+
<Badge selected={o.value === value} onClick={handleClick(o.value)}>
52+
{o.label}
53+
</Badge>
54+
</Tip>
5155
))}
5256
</Wrapper>
5357
);

app/src/store/stores/batchStore.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
entries,
23
makeAutoObservable,
34
observable,
45
ObservableMap,
@@ -69,6 +70,13 @@ export default class BatchStore {
6970
);
7071
}
7172

73+
/** the collection of lease durations sorted by number of blocks */
74+
get sortedDurations() {
75+
return entries(this.leaseDurations)
76+
.map(([duration, state]) => ({ duration, state }))
77+
.sort((a, b) => a.duration - b.duration);
78+
}
79+
7280
/**
7381
* the collection of batches for the active market
7482
*/

app/src/store/views/batchesView.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { entries, makeAutoObservable } from 'mobx';
1+
import { makeAutoObservable } from 'mobx';
22
import {
33
DurationBucketState,
44
NodeTier,
55
} from 'types/generated/auctioneerrpc/auctioneer_pb';
66
import { toPercent } from 'util/bigmath';
7+
import { blocksToTime } from 'util/formatters';
78
import { Store } from 'store';
89

910
export default class BatchesView {
@@ -85,16 +86,17 @@ export default class BatchesView {
8586

8687
/** the markets that are currently open (accepting & matching orders) */
8788
get openMarkets() {
88-
return entries(this._store.batchStore.leaseDurations)
89-
.map(([duration, state]) => ({ duration, state }))
90-
.filter(({ state }) => state === DurationBucketState.MARKET_OPEN);
89+
return this._store.batchStore.sortedDurations.filter(
90+
({ state }) => state === DurationBucketState.MARKET_OPEN,
91+
);
9192
}
9293

9394
/** the list of markets to display as badges */
9495
get marketOptions() {
9596
return this.openMarkets.map(({ duration }) => ({
96-
label: `${duration}`,
97+
label: blocksToTime(duration),
9798
value: `${duration}`,
99+
tip: `${duration} blocks`,
98100
}));
99101
}
100102

app/src/store/views/orderFormView.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { entries, makeAutoObservable, runInAction } from 'mobx';
1+
import { makeAutoObservable, runInAction } from 'mobx';
22
import {
33
DurationBucketState,
44
NodeTier,
@@ -8,6 +8,7 @@ import Big from 'big.js';
88
import debounce from 'lodash/debounce';
99
import { annualPercentRate, toBasisPoints, toPercent } from 'util/bigmath';
1010
import { BLOCKS_PER_DAY } from 'util/constants';
11+
import { blocksToTime } from 'util/formatters';
1112
import { prefixTranslation } from 'util/translate';
1213
import { ONE_UNIT } from 'api/pool';
1314
import { Store } from 'store';
@@ -93,9 +94,9 @@ export default class OrderFormView {
9394
/** the markets currently open or accepting orders */
9495
get marketsAcceptingOrders() {
9596
const { MARKET_OPEN, ACCEPTING_ORDERS } = DurationBucketState;
96-
return entries(this._store.batchStore.leaseDurations)
97-
.map(([duration, state]) => ({ duration, state }))
98-
.filter(({ state }) => state === MARKET_OPEN || state === ACCEPTING_ORDERS);
97+
return this._store.batchStore.sortedDurations.filter(
98+
({ state }) => state === MARKET_OPEN || state === ACCEPTING_ORDERS,
99+
);
99100
}
100101

101102
/** the mapping of market states to user-friendly labels */
@@ -112,7 +113,7 @@ export default class OrderFormView {
112113
get durationOptions() {
113114
const labels = this.marketStateLabels;
114115
const durations = this.marketsAcceptingOrders.map(({ duration, state }) => ({
115-
label: `${duration} (${labels[state]})`,
116+
label: `${blocksToTime(duration)} (${labels[state]})`,
116117
value: `${duration}`,
117118
}));
118119

@@ -122,7 +123,9 @@ export default class OrderFormView {
122123
// add a default option with a value of zero to signify that the duration
123124
// currently being displayed should be used
124125
durations.unshift({
125-
label: `${l('inView')} (${selectedDuration}, ${labels[selectedState]})`,
126+
label: `${l('inView')} (${blocksToTime(selectedDuration)}, ${
127+
labels[selectedState]
128+
})`,
126129
value: '0',
127130
});
128131
}

app/src/util/formatters.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Big from 'big.js';
2-
import { Unit, Units } from './constants';
2+
import { BLOCKS_PER_DAY, Unit, Units } from './constants';
3+
import { plural } from './strings';
34

45
interface FormatSatsOptions {
56
/** the units to convert the sats to (defaults to `sats`) */
@@ -65,3 +66,23 @@ export const formatTime = (totalSeconds: number) => {
6566

6667
return parts.join(' ');
6768
};
69+
70+
/**
71+
* Converts a number of blocks to a human readable time in weeks, months, or years
72+
* @param blocks the number of blocks
73+
*/
74+
export const blocksToTime = (blocks: number) => {
75+
const days = Math.round(blocks / BLOCKS_PER_DAY);
76+
if (days < 7) {
77+
return `${days} ${plural(days, 'day')}`;
78+
} else if (days < 28) {
79+
const weeks = Math.round(days / 7);
80+
return `${weeks} ${plural(weeks, 'week')}`;
81+
} else if (days < 365 - 30) {
82+
const months = Math.round(days / 30);
83+
return `${months} ${plural(months, 'month')}`;
84+
} else {
85+
const years = Math.round(days / 365);
86+
return `${years} ${plural(years, 'year')}`;
87+
}
88+
};

app/src/util/strings.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,97 @@ export const ellipseInside = (
2525
return `${firstChars}...${lastChars}`;
2626
};
2727

28+
/**
29+
* Returns the plural of an English word.
30+
*
31+
* @export
32+
* @param {string} word
33+
* @param {number} [amount]
34+
* @returns {string}
35+
*/
36+
export const plural = (amount: number, word: string): string => {
37+
if (amount === 1) {
38+
return word;
39+
}
40+
const plural: { [key: string]: string } = {
41+
'(quiz)$': '$1zes',
42+
'^(ox)$': '$1en',
43+
'([m|l])ouse$': '$1ice',
44+
'(matr|vert|ind)ix|ex$': '$1ices',
45+
'(x|ch|ss|sh)$': '$1es',
46+
'([^aeiouy]|qu)y$': '$1ies',
47+
'(hive)$': '$1s',
48+
'(?:([^f])fe|([lr])f)$': '$1$2ves',
49+
'(shea|lea|loa|thie)f$': '$1ves',
50+
sis$: 'ses',
51+
'([ti])um$': '$1a',
52+
'(tomat|potat|ech|her|vet)o$': '$1oes',
53+
'(bu)s$': '$1ses',
54+
'(alias)$': '$1es',
55+
'(octop)us$': '$1i',
56+
'(ax|test)is$': '$1es',
57+
'(us)$': '$1es',
58+
'([^s]+)$': '$1s',
59+
};
60+
const irregular: { [key: string]: string } = {
61+
move: 'moves',
62+
foot: 'feet',
63+
goose: 'geese',
64+
sex: 'sexes',
65+
child: 'children',
66+
man: 'men',
67+
tooth: 'teeth',
68+
person: 'people',
69+
};
70+
const uncountable: string[] = [
71+
'sheep',
72+
'fish',
73+
'deer',
74+
'moose',
75+
'series',
76+
'species',
77+
'money',
78+
'rice',
79+
'information',
80+
'equipment',
81+
'bison',
82+
'cod',
83+
'offspring',
84+
'pike',
85+
'salmon',
86+
'shrimp',
87+
'swine',
88+
'trout',
89+
'aircraft',
90+
'hovercraft',
91+
'spacecraft',
92+
'sugar',
93+
'tuna',
94+
'you',
95+
'wood',
96+
];
97+
// save some time in the case that singular and plural are the same
98+
if (uncountable.indexOf(word.toLowerCase()) >= 0) {
99+
return word;
100+
}
101+
// check for irregular forms
102+
for (const w in irregular) {
103+
const pattern = new RegExp(`${w}$`, 'i');
104+
const replace = irregular[w];
105+
if (pattern.test(word)) {
106+
return word.replace(pattern, replace);
107+
}
108+
}
109+
// check for matches using regular expressions
110+
for (const reg in plural) {
111+
const pattern = new RegExp(reg, 'i');
112+
if (pattern.test(word)) {
113+
return word.replace(pattern, plural[reg]);
114+
}
115+
}
116+
return word;
117+
};
118+
28119
/**
29120
* Extracts the domain name from a full url
30121
*/

0 commit comments

Comments
 (0)