Skip to content

Commit 846e69d

Browse files
committed
feat(common): add includeCount option to pluralize function
- Add optional { includeCount: false } parameter to return just the pluralized word without the count - Update ask-user-branch.tsx to use new option instead of hacky .slice(2) - Add tests for the new parameter
1 parent e160a8a commit 846e69d

File tree

3 files changed

+389
-21
lines changed

3 files changed

+389
-21
lines changed

cli/src/components/blocks/ask-user-branch.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { pluralize } from '@codebuff/common/util/string'
12
import { TextAttributes } from '@opentui/core'
23
import React from 'react'
34

@@ -30,12 +31,12 @@ export const AskUserBranch = ({ block, availableWidth }: AskUserBranchProps) =>
3031
>
3132
{block.skipped ? (
3233
<text style={{ fg: theme.muted, attributes: TextAttributes.ITALIC }}>
33-
You skipped the questions.
34+
You skipped the {pluralize(block.questions.length, 'question', { includeCount: false })}.
3435
</text>
3536
) : (
3637
<box style={{ flexDirection: 'column', gap: 1 }}>
3738
<text style={{ fg: theme.secondary, attributes: TextAttributes.BOLD }}>
38-
Your answers:
39+
Your {pluralize(block.questions.length, 'answer', { includeCount: false })}:
3940
</text>
4041
{block.questions.map((q, idx) => {
4142
const answer = block.answers?.find((a) => a.questionIndex === idx)

common/src/util/__tests__/string.test.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,204 @@ describe('pluralize', () => {
3838
expect(pluralize(5, 'member')).toBe('5 members')
3939
expect(pluralize(10, 'invitation')).toBe('10 invitations')
4040
})
41+
42+
it('should return only the word when includeCount is false', () => {
43+
expect(pluralize(1, 'answer', { includeCount: false })).toBe('answer')
44+
expect(pluralize(2, 'answer', { includeCount: false })).toBe('answers')
45+
expect(pluralize(1, 'question', { includeCount: false })).toBe('question')
46+
expect(pluralize(5, 'question', { includeCount: false })).toBe('questions')
47+
expect(pluralize(2, 'city', { includeCount: false })).toBe('cities')
48+
expect(pluralize(2, 'leaf', { includeCount: false })).toBe('leaves')
49+
})
50+
51+
// Tech/CS irregular plurals (truly irregular, no derivable pattern)
52+
it('should handle truly irregular plurals', () => {
53+
// Common irregulars
54+
expect(pluralize(2, 'person')).toBe('2 people')
55+
expect(pluralize(2, 'child')).toBe('2 children')
56+
expect(pluralize(2, 'mouse')).toBe('2 mice')
57+
58+
// -ex/-ix → -ices (no reliable rule, must be hardcoded)
59+
expect(pluralize(2, 'index')).toBe('2 indices')
60+
expect(pluralize(2, 'vertex')).toBe('2 vertices')
61+
expect(pluralize(2, 'matrix')).toBe('2 matrices')
62+
expect(pluralize(2, 'appendix')).toBe('2 appendices')
63+
64+
// Latin -um → -a
65+
expect(pluralize(2, 'datum')).toBe('2 data')
66+
expect(pluralize(2, 'medium')).toBe('2 media')
67+
expect(pluralize(2, 'criterion')).toBe('2 criteria')
68+
expect(pluralize(2, 'phenomenon')).toBe('2 phenomena')
69+
})
70+
71+
// Derived rule: -sis → -ses
72+
it('should handle -sis → -ses by rule', () => {
73+
expect(pluralize(2, 'analysis')).toBe('2 analyses')
74+
expect(pluralize(2, 'basis')).toBe('2 bases')
75+
expect(pluralize(2, 'hypothesis')).toBe('2 hypotheses')
76+
expect(pluralize(2, 'thesis')).toBe('2 theses')
77+
expect(pluralize(2, 'parenthesis')).toBe('2 parentheses')
78+
expect(pluralize(2, 'synopsis')).toBe('2 synopses')
79+
expect(pluralize(2, 'crisis')).toBe('2 crises')
80+
expect(pluralize(2, 'diagnosis')).toBe('2 diagnoses')
81+
expect(pluralize(2, 'ellipsis')).toBe('2 ellipses')
82+
// Any new -sis word should work without adding to list
83+
expect(pluralize(2, 'oasis')).toBe('2 oases')
84+
expect(pluralize(2, 'genesis')).toBe('2 geneses')
85+
})
86+
87+
// Derived rule: -xis → -xes
88+
it('should handle -xis → -xes by rule', () => {
89+
expect(pluralize(2, 'axis')).toBe('2 axes')
90+
// Any new -xis word should work without adding to list
91+
expect(pluralize(2, 'praxis')).toBe('2 praxes')
92+
})
93+
94+
// Derived rule: -ware stays unchanged
95+
it('should handle -ware words staying unchanged by rule', () => {
96+
expect(pluralize(2, 'software')).toBe('2 software')
97+
expect(pluralize(2, 'hardware')).toBe('2 hardware')
98+
expect(pluralize(2, 'firmware')).toBe('2 firmware')
99+
expect(pluralize(2, 'malware')).toBe('2 malware')
100+
expect(pluralize(2, 'middleware')).toBe('2 middleware')
101+
expect(pluralize(2, 'freeware')).toBe('2 freeware')
102+
expect(pluralize(2, 'shareware')).toBe('2 shareware')
103+
// Any new -ware word should work without adding to list
104+
expect(pluralize(2, 'spyware')).toBe('2 spyware')
105+
expect(pluralize(2, 'bloatware')).toBe('2 bloatware')
106+
})
107+
108+
// Derived rule: -ics stays unchanged
109+
it('should handle -ics words staying unchanged by rule', () => {
110+
expect(pluralize(2, 'analytics')).toBe('2 analytics')
111+
expect(pluralize(2, 'graphics')).toBe('2 graphics')
112+
expect(pluralize(2, 'physics')).toBe('2 physics')
113+
expect(pluralize(2, 'mathematics')).toBe('2 mathematics')
114+
expect(pluralize(2, 'statistics')).toBe('2 statistics')
115+
expect(pluralize(2, 'logistics')).toBe('2 logistics')
116+
expect(pluralize(2, 'economics')).toBe('2 economics')
117+
// Any new -ics word should work without adding to list
118+
expect(pluralize(2, 'semantics')).toBe('2 semantics')
119+
expect(pluralize(2, 'heuristics')).toBe('2 heuristics')
120+
})
121+
122+
it('should handle other unchanging plurals', () => {
123+
// Data terms (hardcoded because 'data' is special case)
124+
expect(pluralize(2, 'data')).toBe('2 data')
125+
expect(pluralize(2, 'metadata')).toBe('2 metadata')
126+
expect(pluralize(2, 'feedback')).toBe('2 feedback')
127+
128+
// Other words ending in -s that don't change
129+
expect(pluralize(2, 'series')).toBe('2 series')
130+
expect(pluralize(2, 'chassis')).toBe('2 chassis')
131+
expect(pluralize(2, 'species')).toBe('2 species')
132+
})
133+
134+
it('should handle tech -o endings', () => {
135+
// Words that add -es (default for -o)
136+
expect(pluralize(2, 'hero')).toBe('2 heroes')
137+
expect(pluralize(2, 'echo')).toBe('2 echoes')
138+
expect(pluralize(2, 'veto')).toBe('2 vetoes')
139+
140+
// Tech terms that just add -s
141+
expect(pluralize(2, 'photo')).toBe('2 photos')
142+
expect(pluralize(2, 'video')).toBe('2 videos')
143+
expect(pluralize(2, 'audio')).toBe('2 audios')
144+
expect(pluralize(2, 'logo')).toBe('2 logos')
145+
expect(pluralize(2, 'demo')).toBe('2 demos')
146+
expect(pluralize(2, 'repo')).toBe('2 repos')
147+
expect(pluralize(2, 'memo')).toBe('2 memos')
148+
expect(pluralize(2, 'typo')).toBe('2 typos')
149+
expect(pluralize(2, 'intro')).toBe('2 intros')
150+
expect(pluralize(2, 'macro')).toBe('2 macros')
151+
expect(pluralize(2, 'scenario')).toBe('2 scenarios')
152+
expect(pluralize(2, 'portfolio')).toBe('2 portfolios')
153+
expect(pluralize(2, 'ratio')).toBe('2 ratios')
154+
expect(pluralize(2, 'zero')).toBe('2 zeros')
155+
expect(pluralize(2, 'silo')).toBe('2 silos') // data silos
156+
})
157+
158+
it('should handle -f/-fe endings', () => {
159+
// Words that change -f to -ves (default behavior)
160+
expect(pluralize(2, 'half')).toBe('2 halves')
161+
expect(pluralize(2, 'shelf')).toBe('2 shelves')
162+
expect(pluralize(2, 'self')).toBe('2 selves')
163+
expect(pluralize(2, 'leaf')).toBe('2 leaves')
164+
165+
// -fe to -ves
166+
expect(pluralize(2, 'knife')).toBe('2 knives')
167+
expect(pluralize(2, 'life')).toBe('2 lives')
168+
169+
// Tech/design terms that just add -s
170+
expect(pluralize(2, 'proof')).toBe('2 proofs') // mathematical proofs
171+
expect(pluralize(2, 'brief')).toBe('2 briefs') // design briefs
172+
expect(pluralize(2, 'chief')).toBe('2 chiefs') // tech leads
173+
expect(pluralize(2, 'staff')).toBe('2 staffs')
174+
expect(pluralize(2, 'serif')).toBe('2 serifs') // font serifs
175+
expect(pluralize(2, 'motif')).toBe('2 motifs') // design motifs
176+
expect(pluralize(2, 'gif')).toBe('2 gifs')
177+
expect(pluralize(2, 'pdf')).toBe('2 pdfs')
178+
})
179+
180+
it('should handle words ending in -y with vowel before', () => {
181+
// vowel + y -> just add -s
182+
expect(pluralize(2, 'key')).toBe('2 keys')
183+
expect(pluralize(2, 'monkey')).toBe('2 monkeys')
184+
expect(pluralize(2, 'toy')).toBe('2 toys')
185+
expect(pluralize(2, 'boy')).toBe('2 boys')
186+
expect(pluralize(2, 'day')).toBe('2 days')
187+
expect(pluralize(2, 'way')).toBe('2 ways')
188+
expect(pluralize(2, 'tray')).toBe('2 trays')
189+
expect(pluralize(2, 'valley')).toBe('2 valleys')
190+
expect(pluralize(2, 'donkey')).toBe('2 donkeys')
191+
expect(pluralize(2, 'essay')).toBe('2 essays')
192+
})
193+
194+
it('should handle words ending in -ful', () => {
195+
expect(pluralize(2, 'cupful')).toBe('2 cupfuls')
196+
expect(pluralize(2, 'handful')).toBe('2 handfuls')
197+
expect(pluralize(2, 'spoonful')).toBe('2 spoonfuls')
198+
})
199+
200+
it('should handle words already ending in -s', () => {
201+
expect(pluralize(2, 'bus')).toBe('2 buses')
202+
expect(pluralize(2, 'lens')).toBe('2 lenses')
203+
expect(pluralize(2, 'gas')).toBe('2 gases')
204+
})
205+
206+
it('should handle words ending in -z', () => {
207+
// Single z doubles
208+
expect(pluralize(2, 'quiz')).toBe('2 quizzes')
209+
expect(pluralize(2, 'fez')).toBe('2 fezzes')
210+
// Double z just adds -es
211+
expect(pluralize(2, 'buzz')).toBe('2 buzzes')
212+
expect(pluralize(2, 'fizz')).toBe('2 fizzes')
213+
})
214+
215+
it('should handle common tech terms', () => {
216+
// Regular tech plurals
217+
expect(pluralize(2, 'schema')).toBe('2 schemas')
218+
expect(pluralize(2, 'API')).toBe('2 APIs')
219+
expect(pluralize(2, 'SDK')).toBe('2 SDKs')
220+
expect(pluralize(2, 'URL')).toBe('2 URLs')
221+
expect(pluralize(2, 'CLI')).toBe('2 CLIs')
222+
expect(pluralize(2, 'bug')).toBe('2 bugs')
223+
expect(pluralize(2, 'feature')).toBe('2 features')
224+
expect(pluralize(2, 'commit')).toBe('2 commits')
225+
expect(pluralize(2, 'branch')).toBe('2 branches')
226+
expect(pluralize(2, 'merge')).toBe('2 merges')
227+
expect(pluralize(2, 'deploy')).toBe('2 deploys')
228+
expect(pluralize(2, 'release')).toBe('2 releases')
229+
expect(pluralize(2, 'sprint')).toBe('2 sprints')
230+
expect(pluralize(2, 'ticket')).toBe('2 tickets')
231+
expect(pluralize(2, 'endpoint')).toBe('2 endpoints')
232+
expect(pluralize(2, 'webhook')).toBe('2 webhooks')
233+
expect(pluralize(2, 'callback')).toBe('2 callbacks')
234+
expect(pluralize(2, 'payload')).toBe('2 payloads')
235+
expect(pluralize(2, 'token')).toBe('2 tokens')
236+
expect(pluralize(2, 'query')).toBe('2 queries')
237+
expect(pluralize(2, 'dependency')).toBe('2 dependencies')
238+
})
41239
})
42240

43241
describe('replaceNonStandardPlaceholderComments', () => {

0 commit comments

Comments
 (0)