About me

About me

Feeds

RSS feed

Singulars and plurals

18th December 2025

This is a pair of utilities I wrote some time ago to handle image searches, but I thought it would be worth sharing in case it's useful for other applications.

It consists of two functions:

  • pluralise takes an English word and returns its plural.

If it's already plural it just returns it.

  • singularise takes an English word and returns its singular.

If it's already singular it just returns it.

The typical situation is that you have a library of images tagged with keywords, but for an image containing cats it's inefficient to have to add both the tags "cat" and "cats". These functions ensure that a search for either "cat" or "cats" will find the appropriate image.

For example:

> (pluralise "cat")
"cats"
> (pluralise "mouse")
"mice"

and the reverse function:

> (singularise "cats")
"cat"
> (singularise "mice")
"mouse"

Here are the functions:

(defparameter *plurals*
  '(("mouse" . "mice") ("person" . "people") ("man" . "men") ("woman" . "women")
("child" . "children") ("bus" . "buses") ("fish" . "fish") ("piano" . "pianos")
("leaf" . "leaves") ("wildlife" . "wildlife") ("goose" . "geese") ("paper" . "paper"))) (defun pluralise (string) (let ((l (length string))) (cond ((<= l 1) string) (t (let* ((keyword (string-downcase string)) (last (char keyword (1- l))) (irregular (assoc keyword *plurals* :test #'equal))) (cond ;; Already a plural ((rassoc keyword *plurals* :test #'equal) keyword) ;; Irregulars (irregular (cdr irregular)) ;; dress -> dresses etc ((string= "ss" keyword :start2 (- l 2)) (concatenate 'string keyword "es")) ;; Already a plural ((eq last #\s) keyword) ;; dish -> dishes etc ((string= "sh" keyword :start2 (- l 2)) (concatenate 'string keyword "es")) ;; baby -> babies but key -> keys ((and (eq last #\y) (not (find (char keyword (- l 2)) "aeiou"))) (concatenate 'string (subseq keyword 0 (1- l)) "ies")) ;; mango -> mangoes etc ((eq last #\o) (concatenate 'string (subseq keyword 0 (1- l)) "oes")) ;; Otherwise just add s (t (concatenate 'string keyword "s")))))))) (defun singularise (string) (let ((l (length string))) (cond ((<= l 2) string) (t (let* ((keyword (string-downcase string)) (last (char keyword (1- l))) (irregular (rassoc keyword *plurals* :test #'equal))) (cond ;; Already singular ((assoc keyword *plurals* :test #'equal) keyword) ;; Irregulars (irregular (car irregular)) ;; horses -> horse etc ((string= "ses" keyword :start2 (- l 3)) (subseq keyword 0 (- l 1))) ;; bushes -> bush etc ((string= "hes" keyword :start2 (- l 3)) (subseq keyword 0 (- l 2))) ;; babies -> baby etc ((string= "ies" keyword :start2 (- l 3))
(concatenate 'string (subseq keyword 0 (- l 3)) "y")) ;; shoes -> shoe etc ((string= "oes" keyword :start2 (- l 3)) (subseq keyword 0 (- l 1))) ;; dresses -> dress etc ((string= "ss" keyword :start2 (- l 2)) keyword) ;; Otherwise just remove s ((eq last #\s) (subseq keyword 0 (1- l))) ;; or leave as is (t keyword)))))))

You can extend it for words it doesn't handle correctly by adding them to the *plurals* alist.


blog comments powered by Disqus