Skip to content

Commit dc11fd9

Browse files
committed
feat: Replace class with pure functions
1 parent c17dc23 commit dc11fd9

6 files changed

Lines changed: 110 additions & 140 deletions

File tree

README.md

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,16 @@ yarn add @untemps/read-per-minute
1414

1515
## Usage
1616

17-
Import `ReadPerMinute`:
17+
Import the functions:
1818

1919
```javascript
20-
import { ReadPerMinute } from '@untemps/read-per-minute'
20+
import { parse, isLangExist, rates } from '@untemps/read-per-minute'
2121
```
2222

23-
Create an instance of `ReadPerMinute`:
23+
Call `parse` with a string and a lang (see [Override the value for a one-time parsing](#override-the-value-for-a-one-time-parsing) for an alternative usage):
2424

2525
```javascript
26-
const rpm = new ReadPerMinute()
27-
```
28-
29-
Call the `parse` method with a string and a lang (see [Override the value for a one-time parsing](#override-the-value-for-a-one-time-parsing) for an alternative usage):
30-
31-
```javascript
32-
rpm.parse('Long text', 'en')
26+
parse('Long text', 'en')
3327
```
3428

3529
The `parse` function returns an object with the following properties:
@@ -66,7 +60,7 @@ If the lang is not listed or is undefined, the default value will be used instea
6660

6761
### Override all the values
6862

69-
You can specify an entire custom rates object in the constructor of an instance:
63+
You can specify an entire custom rates object as the third parameter of `parse` or `isLangExist`:
7064

7165
```javascript
7266
const customRates = {
@@ -76,19 +70,20 @@ const customRates = {
7670
nl: 234,
7771
en: 244,
7872
}
79-
const rpm = new ReadPerMinute(customRates)
73+
parse('Long text', 'en', customRates)
74+
isLangExist('en', customRates)
8075
```
8176

82-
**NOTE**: Set a `default` property in the object if you want the parsing to fallback to a specific value.
83-
Otherwise, the static value will be used (`ReadPerMinute.rates.default`).
77+
**NOTE**: Set a `default` property in the object if you want the parsing to fallback to a specific value.
78+
Otherwise, the static value will be used (`rates.default`).
8479

8580
### Override the value for a one-time parsing
8681

8782
Simply pass the desired custom reading rate in words per minute instead of a language code:
8883

8984
```javascript
9085
// For very fast readers: 425 words per minute.
91-
rpm.parse('Long text', 425)
86+
parse('Long text', 425)
9287
```
9388

9489
**NOTE**: The custom reading rate must be greater than zero or the default value will be used.

src/ReadPerMinute.d.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,20 @@ export type Rates = Readonly<
1616
} & Record<string, number>
1717
>;
1818

19-
export default class ReadPerMinute {
20-
static readonly rates: Rates;
19+
export const rates: Rates;
2120

22-
constructor(rates?: Partial<Rates> | null);
23-
24-
/** Returns whether the specified lang is indexed in the rate list. */
25-
isLangExist(lang?: string): boolean;
26-
27-
/**
28-
* Parses a string, counts the number of words and divides it by the lang rate
29-
* to get an estimated reading time.
30-
*/
31-
parse(
32-
text?: string | null,
33-
langOrRate?: string | number | null
34-
): { time: number; words: number; rate: number };
35-
}
21+
/** Returns whether the specified lang is indexed in the rate list. */
22+
export function isLangExist(
23+
lang?: string | null,
24+
customRates?: Partial<Rates> | null
25+
): boolean;
3626

27+
/**
28+
* Parses a string, counts the number of words and divides it by the lang rate
29+
* to get an estimated reading time.
30+
*/
31+
export function parse(
32+
text?: string | null,
33+
langOrRate?: string | number | null,
34+
customRates?: Partial<Rates> | null
35+
): { time: number; words: number; rate: number };

src/ReadPerMinute.js

Lines changed: 52 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,62 @@
11
/**
22
* Rates from "How many words do we read per minute? A review and meta-analysis of reading rate" by Marc Brysbaert - Department of Experimental Psychology Ghent University
33
*/
4-
class ReadPerMinute {
5-
static rates = Object.freeze({
6-
default: 200,
7-
ar: 181,
8-
zh: 260,
9-
nl: 228,
10-
en: 236,
11-
fi: 195,
12-
fr: 214,
13-
de: 260,
14-
he: 224,
15-
it: 285,
16-
ko: 226,
17-
es: 278,
18-
sv: 218,
19-
})
4+
export const rates = Object.freeze({
5+
default: 200,
6+
ar: 181,
7+
zh: 260,
8+
nl: 228,
9+
en: 236,
10+
fi: 195,
11+
fr: 214,
12+
de: 260,
13+
he: 224,
14+
it: 285,
15+
ko: 226,
16+
es: 278,
17+
sv: 218,
18+
})
2019

21-
_rates = null
20+
const mergeRates = (customRates) => {
21+
if (customRates && typeof customRates === 'object' && !Array.isArray(customRates)) {
22+
return { ...rates, ...customRates }
23+
}
24+
return rates
25+
}
2226

23-
constructor(rates = null) {
24-
this._rates = rates ?? ReadPerMinute.rates
25-
}
27+
/**
28+
* Returns whether the specified lang is indexed in the rate list.
29+
* @param {string} lang Lang to test
30+
* @param {object|null} customRates Optional custom rates to check in
31+
* @returns {boolean} True if the lang is indexed, false if not
32+
*/
33+
export const isLangExist = (lang, customRates = null) => {
34+
const r = mergeRates(customRates)
35+
return !!lang && !!r[lang]
36+
}
2637

27-
/**
28-
* Returns whether the specified lang is indexed in the rate list.
29-
* @param lang {string} Lang to test
30-
* @returns {boolean} True if the lang is indexed, false if not
31-
*/
32-
isLangExist(lang) {
33-
return !!lang && !!this._rates[lang]
34-
}
38+
/**
39+
* Parses a string, counts the number the words and divides it by the lang rate to get an estimated reading time.
40+
* The function returns an object containing the estimated time, the number of words and the rate used in the calculation.
41+
* @param {string} text String to parse.
42+
* @param {string|number} langOrRate Lang used to retrieve the reading rate, or a custom rate value.
43+
* @param {object|null} customRates Optional custom rates overriding defaults.
44+
* @returns {{rate: number, words: number, time: number}} Object containing the estimated time, the number of words and the rate used in the calculation.
45+
*/
46+
export const parse = (text = '', langOrRate = 'default', customRates = null) => {
47+
const r = mergeRates(customRates)
3548

36-
/**
37-
* Parses a string, counts the number the words and divides it by the lang rate to get an estimated reading time.
38-
* The function returns an object containing the estimated time, the number of words and the rate used in the calculation.
39-
* @param text {string} String to parse.
40-
* @param langOrRate {string | number} Lang used to retrieve the reading rate, or a custom rate value.
41-
* @returns {{rate: number, words: number, time: number}} Object containing the estimated time, the number of words and the rate used in the calculation.
42-
*/
43-
parse(text = '', langOrRate = 'default') {
44-
let rate = this._rates['default'] ?? ReadPerMinute.rates['default'];
45-
if (+langOrRate > 0) {
46-
rate = langOrRate
47-
} else if (this.isLangExist(langOrRate)) {
48-
rate = this._rates[langOrRate]
49-
}
49+
let rate = r['default'] ?? rates['default']
50+
if (+langOrRate > 0) {
51+
rate = +langOrRate
52+
} else if (isLangExist(langOrRate, r)) {
53+
rate = r[langOrRate]
54+
}
5055

51-
if (!text?.length) {
52-
return {
53-
time: 0,
54-
words: 0,
55-
rate,
56-
}
57-
}
56+
if (!text?.length) {
57+
return { time: 0, words: 0, rate }
58+
}
5859

59-
const words = text.trim().split(/\s+/).length
60-
return {
61-
time: Math.ceil(words / rate),
62-
words,
63-
rate,
64-
}
65-
}
60+
const words = text.trim().split(/\s+/).length
61+
return { time: Math.ceil(words / rate), words, rate }
6662
}
67-
68-
export default ReadPerMinute

src/__tests__/ReadPerMinute.test.js

Lines changed: 31 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from 'vitest'
22

3-
import ReadPerMinute from '../ReadPerMinute'
3+
import { parse, isLangExist, rates } from '../index'
44

55
const WORDS = [
66
'year',
@@ -73,13 +73,7 @@ const generateTokenizedText = (separator = '%', maxWordCount = 50, minWordCount
7373
return { str, int, tok }
7474
}
7575

76-
describe('ReadPerMinute', () => {
77-
describe('constructor', () => {
78-
test('Instantiates class with no error', () => {
79-
expect(() => new ReadPerMinute()).not.toThrow()
80-
})
81-
})
82-
76+
describe('ReadPerMinute (functions)', () => {
8377
describe('custom rates', () => {
8478
const customRates = {
8579
default: 2000,
@@ -96,36 +90,27 @@ describe('ReadPerMinute', () => {
9690
es: 2780,
9791
sv: 2180,
9892
}
99-
100-
test('Instantiates class with no error', () => {
101-
expect(() => new ReadPerMinute(customRates)).not.toThrow()
102-
})
103-
10493
test.each([
105-
[{}, ReadPerMinute.rates.default],
106-
[[], ReadPerMinute.rates.default],
107-
['', ReadPerMinute.rates.default],
108-
[null, ReadPerMinute.rates.default],
94+
[{}, rates.default],
95+
[[], rates.default],
96+
['', rates.default],
97+
[null, rates.default],
10998
[{ default: 300 }, 300],
11099
])('Parses text using default values', (customRates, expected) => {
111-
const instance = new ReadPerMinute(customRates)
112-
expect(instance.parse()).toEqual({ time: 0, words: 0, rate: expected })
100+
expect(parse(undefined, undefined, customRates)).toEqual({ time: 0, words: 0, rate: expected })
113101
})
114102

115103
test.each([
116104
[customRates, null, customRates.default],
117105
[customRates, 'en', customRates.en],
118106
])('Parses text using custom values', (customRates, lang, expected) => {
119-
const instance = new ReadPerMinute(customRates)
120-
expect(instance.parse('', lang)).toEqual({ time: 0, words: 0, rate: expected })
107+
expect(parse('', lang, customRates)).toEqual({ time: 0, words: 0, rate: expected })
121108
})
122109
})
123110

124111
describe('isLangExist', () => {
125-
const instance = new ReadPerMinute()
126-
127112
test('Returns false as lang is not specified', () => {
128-
expect(instance.isLangExist()).toBeFalsy()
113+
expect(isLangExist()).toBeFalsy()
129114
})
130115

131116
test.each([
@@ -135,74 +120,72 @@ describe('ReadPerMinute', () => {
135120
[() => null, false],
136121
[[], false],
137122
[{}, false],
138-
[Object.keys(ReadPerMinute.rates)[1], true],
123+
[Object.keys(rates)[1], true],
139124
['de', true],
140125
])('Returns lang existence', (value, expected) => {
141-
expect(instance.isLangExist(value)).toBe(expected)
126+
expect(isLangExist(value)).toBe(expected)
142127
})
143128
})
144129

145130
describe('parse', () => {
146-
const instance = new ReadPerMinute()
147-
148131
test('Parses text using default values', () => {
149-
expect(instance.parse()).toEqual({ time: 0, words: 0, rate: ReadPerMinute.rates.default })
132+
expect(parse()).toEqual({ time: 0, words: 0, rate: rates.default })
150133
expect(
151-
instance.parse(
152-
generateTokenizedText(null, ReadPerMinute.rates.default, ReadPerMinute.rates.default).str
134+
parse(
135+
generateTokenizedText(null, rates.default, rates.default).str
153136
)
154-
).toEqual({ time: 1, words: ReadPerMinute.rates.default, rate: ReadPerMinute.rates.default })
137+
).toEqual({ time: 1, words: rates.default, rate: rates.default })
155138
})
156139

157140
test.each([
158141
[
159142
generateTokenizedText(null, 255, 255).str,
160143
'en',
161-
{ time: Math.ceil(255 / ReadPerMinute.rates.en), words: 255, rate: ReadPerMinute.rates.en },
144+
{ time: Math.ceil(255 / rates.en), words: 255, rate: rates.en },
162145
],
163146
[
164-
generateTokenizedText(null, ReadPerMinute.rates.fr, ReadPerMinute.rates.fr).str,
147+
generateTokenizedText(null, rates.fr, rates.fr).str,
165148
'fr',
166-
{ time: 1, words: ReadPerMinute.rates.fr, rate: ReadPerMinute.rates.fr },
149+
{ time: 1, words: rates.fr, rate: rates.fr },
167150
],
168-
[null, 'en', { time: 0, words: 0, rate: ReadPerMinute.rates.en }],
151+
[null, 'en', { time: 0, words: 0, rate: rates.en }],
169152
[
170-
generateTokenizedText(null, ReadPerMinute.rates.default, ReadPerMinute.rates.default).str,
153+
generateTokenizedText(null, rates.default, rates.default).str,
171154
null,
172-
{ time: 1, words: ReadPerMinute.rates.default, rate: ReadPerMinute.rates.default },
155+
{ time: 1, words: rates.default, rate: rates.default },
173156
],
174157
[
175-
generateTokenizedText(null, ReadPerMinute.rates.default, ReadPerMinute.rates.default).str,
158+
generateTokenizedText(null, rates.default, rates.default).str,
176159
'foo',
177-
{ time: 1, words: ReadPerMinute.rates.default, rate: ReadPerMinute.rates.default },
160+
{ time: 1, words: rates.default, rate: rates.default },
178161
],
179-
[null, null, { time: 0, words: 0, rate: ReadPerMinute.rates.default }],
162+
[null, null, { time: 0, words: 0, rate: rates.default }],
180163
])('Parses text using specified values', (text, lang, expected) => {
181-
expect(instance.parse(text, lang)).toEqual(expected)
164+
expect(parse(text, lang)).toEqual(expected)
182165
})
183166

184167
test.each([
185168
[
186-
generateTokenizedText(null, ReadPerMinute.rates.en, ReadPerMinute.rates.en).str,
169+
generateTokenizedText(null, rates.en, rates.en).str,
187170
'fr',
188-
{ time: 1, words: ReadPerMinute.rates.en, rate: ReadPerMinute.rates.en },
171+
{ time: 1, words: rates.en, rate: rates.en },
189172
],
190173
])('Parses text regarding lang rate', (text, lang, expected) => {
191-
expect(instance.parse(text, lang)).not.toEqual(expected)
174+
expect(parse(text, lang)).not.toEqual(expected)
192175
})
193176

194177
test.each([[generateTokenizedText(null, 425, 425).str, 425, { time: 1, words: 425, rate: 425 }]])(
195178
'Parses text and uses a custom rate',
196179
(text, rate, expected) => {
197-
expect(instance.parse(text, rate)).toEqual(expected)
180+
expect(parse(text, rate)).toEqual(expected)
198181
}
199182
)
200183
test.each([
201184
[generateTokenizedText(null, 1, 1).str, 0],
202185
[generateTokenizedText(null, 1, 1).str, -1],
203186
])('Exchanges invalid numeric values with the default rate', (text, customRate) => {
204-
const result = instance.parse(text, customRate)
205-
expect(result.rate).to.equal(ReadPerMinute.rates.default)
187+
const result = parse(text, customRate)
188+
expect(result.rate).to.equal(rates.default)
206189
})
207190
})
208191
})

src/index.d.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
export { default as ReadPerMinute } from './ReadPerMinute';
2-
1+
export { parse, isLangExist, rates, type Rates } from './ReadPerMinute';

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { default as ReadPerMinute } from './ReadPerMinute'
1+
export { parse, isLangExist, rates } from './ReadPerMinute'

0 commit comments

Comments
 (0)