├── index.js
├── script
└── hook.sh
├── .travis.yml
├── bower.json
├── test
├── plural.js
├── setup.js
├── singular.js
├── uncountable.js
├── irregular.js
├── singularize.js
├── titleize.js
├── gsub.js
├── index.html
├── pluralize.js
├── ordinalize.js
└── inflector.js
├── .tm_properties
├── Makefile
├── gulpfile.js
├── .jscsrc
├── .gitignore
├── LICENSE
├── package.json
├── README.md
└── lib
└── underscore.inflection.js
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = process.env.JSCOV
2 | ? require('./lib-cov')
3 | : require('./lib/underscore.inflection.js');
4 |
--------------------------------------------------------------------------------
/script/hook.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -ex
4 |
5 | npm run jscs
6 | npm test
7 |
8 | node bower.json
9 | node package.json
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: node_js
3 | sudo: false
4 | node_js:
5 | - "5.0"
6 | - "4.0"
7 | - "0.12"
8 | - "0.11"
9 | - "0.10"
10 | before_script:
11 | - npm run jscov
12 | after_script:
13 | - npm run coverage
14 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "underscore.inflection",
3 | "version": "1.3.2",
4 | "dependencies": {
5 | "underscore": "^1.9.1"
6 | },
7 | "main": "lib/underscore.inflection.js",
8 | "ignore": [
9 | "lib-cov",
10 | "test"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/test/plural.js:
--------------------------------------------------------------------------------
1 | describe('#plural', function() {
2 | it('adds a new pluralization rule by explict string', function() {
3 | _.plural('axis', 'axes');
4 | expect(_.pluralize('axis')).to.equal('axes');
5 | });
6 | it('adds a new pluralization rule by regex', function() {
7 | _.plural(/(ax)is$/i, '$1es');
8 | expect(_.pluralize('axis')).to.equal('axes');
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Export underscore globally
3 | */
4 | global._ = require('underscore');
5 |
6 | /**
7 | * Export `expect` globally
8 | */
9 | global.expect = require('chai').expect;
10 |
11 | /**
12 | * Require the subject under test
13 | */
14 | require('..');
15 |
16 | /**
17 | * Reset inflections befor each test
18 | */
19 | beforeEach(function() {
20 | _.resetInflections();
21 | });
22 |
--------------------------------------------------------------------------------
/test/singular.js:
--------------------------------------------------------------------------------
1 | describe('#singular', function() {
2 | it('adds a new singularization rule by explicit string', function() {
3 | _.singular('data', 'datum');
4 | expect(_.singularize('data')).to.equal('datum');
5 | });
6 | it('adds a new singularization rule by regex', function() {
7 | _.singular(/(t)a$/i, '$1um');
8 | expect(_.singularize('data')).to.equal('datum');
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/test/uncountable.js:
--------------------------------------------------------------------------------
1 | describe('#uncountable', function() {
2 | it('notes the word as a special case in pluralization', function() {
3 | _.uncountable('asdf');
4 | expect(_.pluralize('asdf')).to.equal('asdf');
5 | });
6 | it('notes the word as a special case in singularization', function() {
7 | _.uncountable('asdf');
8 | expect(_.singularize('asdf')).to.equal('asdf');
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/test/irregular.js:
--------------------------------------------------------------------------------
1 | describe('#irregular', function() {
2 | it('adds a rule to pluralize the special case', function() {
3 | _.irregular('haxor', 'hax0rs!');
4 | expect(_.pluralize('haxor')).to.equal('hax0rs!');
5 | });
6 | it('adds a rule to singularize the special case', function() {
7 | _.irregular('hax0r!', 'haxors');
8 | expect(_.singularize('haxors')).to.equal('hax0r!');
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/.tm_properties:
--------------------------------------------------------------------------------
1 | excludeInFolderSearch = "{$excludeInFolderSearch,node_modules}"
2 |
3 | excludeInFileChooser = "{$excludeInFileChooser,node_modules}"
4 |
5 | # Settings
6 | softWrap = false
7 | softTabs = true
8 | tabSize = 2
9 | include = "{$include,.jscsrc,.gitignore,.travis.yml}"
10 |
11 | [ text.html.markdown ]
12 | softWrap = true
13 | softTabs = false
14 | tabSize = 2
15 |
16 | [ .jscsrc ]
17 | fileType = "source.json"
18 |
--------------------------------------------------------------------------------
/test/singularize.js:
--------------------------------------------------------------------------------
1 | describe('#singularize', function() {
2 | it('singularizes the given noun', function() {
3 | expect(_.singularize('posts')).to.equal('post');
4 | });
5 | it('returns the same word if it cannot be singularized', function() {
6 | expect(_.singularize('post')).to.equal('post');
7 | });
8 | it('singularizes a word that contains an irregular', function() {
9 | expect(_.singularize('comments')).to.equal('comment');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install: .git/hooks/pre-commit
2 | install: .git/hooks/pre-push
3 |
4 | .git/hooks/pre-commit: script/hook.sh
5 | cp $< $@
6 |
7 | .git/hooks/pre-push: script/hook.sh
8 | cp $< $@
9 |
10 | release-patch:
11 | $(shell npm bin)/mversion patch -m
12 | git push origin master
13 | git push origin --tags
14 | npm publish
15 |
16 | release-minor:
17 | $(shell npm bin)/mversion minor -m
18 | git push origin master
19 | git push origin --tags
20 | npm publish
21 |
22 | release-major:
23 | $(shell npm bin)/mversion major -m
24 | git push origin master
25 | git push origin --tags
26 | npm publish
27 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var jscs = require('gulp-jscs');
3 | var jscov = require('gulp-jscoverage');
4 | var contribs = require('gulp-contribs');
5 |
6 | gulp.task('jscs', function() {
7 | return gulp.src([
8 | 'lib/*.js',
9 | 'test/*.js'
10 | ]).pipe(jscs());
11 | });
12 |
13 | gulp.task('jscov', function() {
14 | return gulp.src('lib/*.js')
15 | .pipe(jscov('index.js'))
16 | .pipe(gulp.dest('./lib-cov'));
17 | });
18 |
19 | gulp.task('contribs', function() {
20 | return gulp.src('README.md')
21 | .pipe(contribs())
22 | .pipe(gulp.dest('./'));
23 | });
24 |
25 | gulp.task('docs', ['contribs']);
26 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "google",
3 | "requireParenthesesAroundIIFE": true,
4 | "requireSpaceAfterLineComment": true,
5 | "requireSpaceBeforeBlockStatements": true,
6 | "maximumLineLength": 120,
7 | "validateLineBreaks": "LF",
8 | "validateIndentation": 2,
9 | "disallowMultipleVarDecl": true,
10 | "disallowEmptyBlocks": true,
11 | "disallowMultipleLineStrings": true,
12 | "disallowSpacesInsideParentheses": true,
13 | "disallowSpacesInsideArrayBrackets": true,
14 | "disallowSpacesInsideObjectBrackets": true,
15 | "disallowPaddingNewlinesInBlocks": true,
16 | "disallowImplicitTypeConversion": [
17 | "numeric",
18 | "boolean",
19 | "binary",
20 | "string"
21 | ],
22 | "disallowKeywords": [
23 | "with"
24 | ],
25 | "excludeFiles": [
26 | "node_modules/**"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by http://www.gitignore.io
2 |
3 | ### Node ###
4 | # Logs
5 | logs
6 | *.log
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 |
13 | # Directory for bower dependencies
14 | bower_components
15 |
16 | # Directory for instrumented libs generated by jscoverage/JSCover
17 | lib-cov
18 |
19 | # Coverage directory used by tools like istanbul
20 | coverage
21 |
22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
23 | .grunt
24 |
25 | # Compiled binary addons (http://nodejs.org/api/addons.html)
26 | build/Release
27 |
28 | # Dependency directory
29 | # Commenting this out is preferred by some people, see
30 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
31 | node_modules
32 |
33 | # Users Environment Variables
34 | .lock-wscript
35 |
36 |
--------------------------------------------------------------------------------
/test/titleize.js:
--------------------------------------------------------------------------------
1 | describe('#titleize', function() {
2 | it('returns non-strings', function() {
3 | expect(_.titleize(5)).to.equal(5);
4 | });
5 | it('returns the empty string when provided with the empty string', function() {
6 | expect(_.titleize('')).to.equal('');
7 | });
8 | it('titleizes a word', function() {
9 | expect(_.titleize('banana')).to.equal('Banana');
10 | });
11 | it('titleizes multiple words', function() {
12 | expect(_.titleize('banana potato fork')).to.equal('Banana Potato Fork');
13 | });
14 | it('does not change the whitespace', function() {
15 | expect(_.titleize('\tbanana\npotato fork\r\n')).to.equal('\tBanana\nPotato Fork\r\n');
16 | });
17 | it('does not alter words that begin with non-alphabetic characters', function() {
18 | expect(_.titleize('123banana')).to.equal('123banana');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/test/gsub.js:
--------------------------------------------------------------------------------
1 | describe('#gsub', function() {
2 | describe('with a regex', function() {
3 | it('replaces one instance of the match', function() {
4 | expect(_.gsub('word', /wo/, 'ne')).to.equal('nerd');
5 | });
6 | it('replaces two instances of the match', function() {
7 | expect(_.gsub('word word', /wo/, 'ne')).to.equal('nerd nerd');
8 | });
9 | it('returns null if no match', function() {
10 | expect(_.gsub('word', /zz/, 'ne')).to.be.null;
11 | });
12 | });
13 | describe('with a string', function() {
14 | it('replaces one instance of the match', function() {
15 | expect(_.gsub('word', 'wo', 'ne')).to.equal('nerd');
16 | });
17 | it('replaces two instances of the match', function() {
18 | expect(_.gsub('word word', 'wo', 'ne')).to.equal('nerd nerd');
19 | });
20 | it('returns null if no match', function() {
21 | expect(_.gsub('word', 'zz', 'ne')).to.be.null;
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Jeremy Ruppel
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Mocha
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "underscore.inflection",
3 | "version": "1.3.3",
4 | "description": "ActiveSupport::Inflector, for underscore!",
5 | "main": "index.js",
6 | "browser": "lib/underscore.inflection.js",
7 | "engines": {
8 | "node": ">0.10.0"
9 | },
10 | "scripts": {
11 | "test": "mocha",
12 | "browser": "open test/index.html",
13 | "coverage": "JSCOV=1 mocha -R mocha-lcov-reporter | coveralls",
14 | "jscov": "gulp jscov",
15 | "jscs": "gulp jscs",
16 | "docs": "gulp docs"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git://github.com/jeremyruppel/underscore.inflection.git"
21 | },
22 | "keywords": [
23 | "underscore",
24 | "inflection",
25 | "inflector",
26 | "activesupport",
27 | "plural",
28 | "pluralize"
29 | ],
30 | "author": "Jeremy Ruppel",
31 | "license": "MIT",
32 | "bugs": {
33 | "url": "https://github.com/jeremyruppel/underscore.inflection/issues"
34 | },
35 | "homepage": "https://github.com/jeremyruppel/underscore.inflection",
36 | "devDependencies": {
37 | "chai": "~2.3.0",
38 | "coveralls": "~2.11.2",
39 | "gulp": "~3.8.11",
40 | "gulp-contribs": "~0.0.2",
41 | "gulp-jscoverage": "~0.1.0",
42 | "gulp-jscs": "~1.6.0",
43 | "jscoverage": "~0.5.9",
44 | "mocha": "~2.2.5",
45 | "mocha-lcov-reporter": "~0.0.2",
46 | "mversion": "^1.10.1",
47 | "underscore": "^1.9.1"
48 | },
49 | "dependencies": {}
50 | }
51 |
--------------------------------------------------------------------------------
/test/pluralize.js:
--------------------------------------------------------------------------------
1 | describe('#pluralize', function() {
2 | it('pluralizes the given noun', function() {
3 | expect(_.pluralize('post')).to.equal('posts');
4 | });
5 | it('returns the same word if it cannot be pluralized', function() {
6 | expect(_.pluralize('posts')).to.equal('posts');
7 | });
8 | describe('with a number', function() {
9 | it('pluralizes the word if not 1', function() {
10 | expect(_.pluralize('post', 0)).to.equal('posts');
11 | });
12 | it('pluralizes the word if not "1"', function() {
13 | expect(_.pluralize('post', '0')).to.equal('posts');
14 | });
15 | it('pluralizes the word if non-1 float', function() {
16 | expect(_.pluralize('post', 1.5)).to.equal('posts');
17 | });
18 | it('singularizes the word if 1', function() {
19 | expect(_.pluralize('posts', 1)).to.equal('post');
20 | });
21 | it('singularizes the word if "1"', function() {
22 | expect(_.pluralize('posts', '1')).to.equal('post');
23 | });
24 | describe('and true', function() {
25 | it('includes the word with the plural', function() {
26 | expect(_.pluralize('post', 0, true)).to.equal('0 posts');
27 | });
28 | it('includes the word with non-1 float', function() {
29 | expect(_.pluralize('post', '1.3', true)).to.equal('1.3 posts');
30 | });
31 | it('includes the word with the singular', function() {
32 | expect(_.pluralize('post', 1, true)).to.equal('1 post');
33 | });
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/ordinalize.js:
--------------------------------------------------------------------------------
1 | describe('#ordinalize', function() {
2 | it('returns a stirng that is not a number or string', function() {
3 | expect(_.ordinalize('hello')).to.equal('hello');
4 | });
5 | it('ordinalizes a number', function() {
6 | expect(_.ordinalize(4)).to.equal('4th');
7 | });
8 | it('ordinalizes a number string', function() {
9 | expect(_.ordinalize('4')).to.equal('4th');
10 | });
11 | it('ordinalizes 0 to "0th"', function() {
12 | expect(_.ordinalize(0)).to.equal('0th');
13 | });
14 | it('ordinalizes 1 to "1st"', function() {
15 | expect(_.ordinalize(1)).to.equal('1st');
16 | });
17 | it('ordinalizes 2 to "2nd', function() {
18 | expect(_.ordinalize(2)).to.equal('2nd');
19 | });
20 | it('ordinalizes 3 to "3rd"', function() {
21 | expect(_.ordinalize(3)).to.equal('3rd');
22 | });
23 | it('ordinalizes 11 to "11th"', function() {
24 | expect(_.ordinalize(11)).to.equal('11th');
25 | });
26 | it('ordinalizes 12 to "12th"', function() {
27 | expect(_.ordinalize(12)).to.equal('12th');
28 | });
29 | it('ordinalizes 13 to "13th"', function() {
30 | expect(_.ordinalize(13)).to.equal('13th');
31 | });
32 | it('ordinalizes 1003 to "1003rd"', function() {
33 | expect(_.ordinalize(1003)).to.equal('1003rd');
34 | });
35 | it('ordinalizes -11 to "-11th', function() {
36 | expect(_.ordinalize(-11)).to.equal('-11th');
37 | });
38 | it('ordinalizes -1021 to "-1021st', function() {
39 | expect(_.ordinalize(-1021)).to.equal('-1021st');
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/test/inflector.js:
--------------------------------------------------------------------------------
1 | describe('inflector', function() {
2 | /**
3 | * Test macro for pluralize & singularize tests
4 | */
5 | function example(method, from, to) {
6 | it(method + 's "' + from + '" to "' + to + '"', function() {
7 | expect(_[method](from)).to.equal(to);
8 | });
9 | }
10 |
11 | describe('plurals', function() {
12 | [
13 | ['rose', 'roses'],
14 | ['axis', 'axes'],
15 | ['virus', 'viri'],
16 | ['alias', 'aliases'],
17 | ['bus', 'buses'],
18 | ['tomato', 'tomatoes'],
19 | ['datum', 'data'],
20 | ['analysis', 'analyses'],
21 | ['life', 'lives'],
22 | ['leaf', 'leaves'],
23 | ['loaf', 'loaves'],
24 | ['thief', 'thieves'],
25 | ['hive', 'hives'],
26 | ['boss', 'bosses'],
27 | ['soliloquy', 'soliloquies'],
28 | ['wish', 'wishes'],
29 | ['vertex', 'vertices'],
30 | ['mouse', 'mice'],
31 | ['ox', 'oxen'],
32 | ['quiz', 'quizzes']
33 | ].forEach(function(word) {
34 | example('pluralize', word[0], word[1]);
35 | });
36 | });
37 | describe('singulars', function() {
38 | [
39 | ['roses', 'rose'],
40 | ['news', 'news'],
41 | ['data', 'datum'],
42 | ['analyses', 'analysis'],
43 | ['hives', 'hive'],
44 | ['soliloquies', 'soliloquy'],
45 | ['series', 'series'],
46 | ['movies', 'movie'],
47 | ['wishes', 'wish'],
48 | ['mice', 'mouse'],
49 | ['buses', 'bus'],
50 | ['shoes', 'shoe'],
51 | ['bosses', 'boss'],
52 | ['boss', 'boss'],
53 | ['crises', 'crisis'],
54 | ['viri', 'virus'],
55 | ['statuses', 'status'],
56 | ['oxen', 'ox'],
57 | ['vertices', 'vertex'],
58 | ['quizzes', 'quiz'],
59 | ['databases', 'database']
60 | ].forEach(function(word) {
61 | example('singularize', word[0], word[1]);
62 | });
63 | });
64 | describe('irregulars', function() {
65 | [
66 | ['person', 'people'],
67 | ['man', 'men'],
68 | ['child', 'children'],
69 | ['sex', 'sexes'],
70 | ['move', 'moves'],
71 | ['cow', 'kine']
72 | ].forEach(function(word) {
73 | example('pluralize', word[0], word[1]);
74 | example('singularize', word[1], word[0]);
75 | });
76 | });
77 | describe('uncountables', function() {
78 | [
79 | 'equipment',
80 | 'information',
81 | 'rice',
82 | 'money',
83 | 'species',
84 | 'series',
85 | 'fish',
86 | 'sheep',
87 | 'jeans'
88 | ].forEach(function(word) {
89 | example('pluralize', word, word);
90 | example('singularize', word, word);
91 | });
92 | });
93 | describe('#resetInflections', function() {
94 | it('resets the default inflections', function() {
95 | _.plural('haxor', 'hax0rs!');
96 | expect(_.pluralize('haxor')).to.equal('hax0rs!');
97 | _.resetInflections();
98 | expect(_.pluralize('haxor')).to.equal('haxors');
99 | });
100 | });
101 | });
102 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## underscore.inflection
2 |
3 | > [![NPM version][npm-badge]][npm]
4 | > [![Build Status][travis-badge]][travis-ci]
5 | > [![Coverage Status][coveralls-badge]][coveralls]
6 |
7 | **Another javascript inflector?!**
8 |
9 | I'll be the first to say it; this isn't the first port of [ActiveSupport::Inflector][activesupport] to js. Not by a long shot. But I'll definitely take [underscore][underscore] mixins over extending String.prototype or other clunky implementations any day.
10 |
11 | Also, this one has tests!
12 |
13 | Inflections
14 | -----------
15 |
16 | The methods listed below are the ones you'll be using 99% of the time.
17 |
18 | ### pluralize
19 |
20 | **Signature:** `_.pluralize(word)`
21 |
22 | `pluralize` pluralizes the string passed to it.
23 |
24 | // functional style
25 | _.pluralize('word'); // => 'words'
26 |
27 | // object-oriented style
28 | _('word').pluralize(); // => 'words'
29 |
30 | It also can accept a number as the second parameter. If a number is provided, it will pluralize the word to match the number.
31 |
32 | _('word').pluralize(0); // => 'words'
33 | _('word').pluralize(1); // => 'word'
34 | _('word').pluralize(1.5); // => 'words'
35 |
36 | Optionally, you can pass `true` as a third parameter. If found, this will include the count with the output.
37 |
38 | _('word').pluralize(0, true); // => '0 words'
39 | _('word').pluralize(1, true); // => '1 word'
40 |
41 | ### singularize
42 |
43 | **Signature:** `_.singularize(word)`
44 |
45 | `singularize` returns the singular version of the plural passed to it.
46 |
47 | // functional style
48 | _.singularize('words'); // => 'word'
49 |
50 | // object-oriented style
51 | _('words').singularize(); // => 'word'
52 |
53 | ### gsub
54 |
55 | **Signature:** `_.gsub(word, rule, replacement)`
56 |
57 | `gsub` is a method that is just slightly different than our standard `String#replace`. The main differences are that it matches globally every time, and if no substitution is made it returns `null`. It accepts a string for `word` and `replacement`, and `rule` can be either a string or a regex.
58 |
59 | // functional style
60 | _.gsub('word', /wo/, 'ne'); // => 'nerd'
61 |
62 | // object-oriented style
63 | _('word').gsub(/wo/, 'ne'); // => 'nerd'
64 |
65 | ### ordinalize
66 |
67 | **Signature:** `_.ordinalize(number)`
68 |
69 | `ordinalize` adds an ordinal suffix to `number`.
70 |
71 | _.ordinalize(1); // => '1st'
72 | _.ordinalize("5"); // => '5th'
73 | _.ordinalize(11); // => '11th'
74 | _.ordinalize(1033); // => '1033rd'
75 | _.ordinalize(-15); // => '-15th'
76 |
77 | ### titleize
78 |
79 | **Signature:** `_.titleize( words )`
80 |
81 | `titleize` capitalizes the first letter of each word in the string `words`. It preserves the existing whitespace.
82 |
83 | _.titleize('banana'); // => 'Banana'
84 | _.titleize('banana potato fork'); // => 'Banana Potato Fork'
85 | _.titleize('banana potato\tfork'); // => 'Banana Potato\tFork'
86 |
87 | ## Configuring the Inflector
88 |
89 | Should you ever need to configure the Inflector beyond the defaults, use these methods to do so:
90 |
91 | ### plural
92 |
93 | **Signature:** `_.plural(rule, replacement)`
94 |
95 | `plural` creates a new pluralization rule for the inflector. `rule` can be either a string or a regex.
96 |
97 | // functional style with explicit string
98 | _.plural('axis', 'axes');
99 |
100 | // object-oriented style with explicit string
101 | _('axis').plural('axes');
102 |
103 | // functional style with regex
104 | _.plural(/(ax)is$/i, '$1es');
105 |
106 | // object-oriented style with regex
107 | _(/(ax)is$/i).plural('$1es');
108 |
109 | ### singular
110 |
111 | **Signature:** `_.singular(rule, replacement)`
112 |
113 | `singular` creates a new singularization rule for the inflector. `rule` can be either a string or a regex.
114 |
115 | // functional style with explicit string
116 | _.singular('data', 'datum');
117 |
118 | // object-oriented style with explicit string
119 | _('data').singular('datum');
120 |
121 | // functional style with regex
122 | _.singular(/(t)a$/i, '$1um');
123 |
124 | // object-oriented style with regex
125 | _(/(t)a$/i).singular('$1um');
126 |
127 | ### irregular
128 |
129 | **Signature:** `_.irregular(singular, plural)`
130 |
131 | `irregular` is a shortcut method to create both a pluralization and singularization rule for the word at the same time. You must supply both the singular form and the plural form as explicit strings.
132 |
133 | // functional style
134 | _.irregular('haxor', 'hax0rs!');
135 |
136 | // object-oriented style
137 | _('haxor').irregular('hax0rs!');
138 |
139 | ### uncountable
140 |
141 | **Signature:** `_.uncountable(word)`
142 |
143 | `uncountable` creates a new uncountable rule for `word`. Uncountable words do not get pluralized or singularized.
144 |
145 | // functional style
146 | _.uncountable('equipment');
147 |
148 | // object-oriented style
149 | _('equipment').uncountable();
150 |
151 | ### resetInflections
152 |
153 | **Signature:** `_.resetInflections()`
154 |
155 | `resetInflections` resets the inflector's rules to their initial state, clearing out any custom rules that have been added.
156 |
157 | ## Thanks to...
158 |
159 | The [Rails][rails] team for [ActiveSupport][activesupport]
160 |
161 | The [DocumentCloud][documentcloud] team for [underscore.js][underscore]
162 |
163 | These other Inflector implementations:
164 |
165 | - [active-support-for-javascript](http://code.google.com/p/active-support-for-javascript/)
166 | - [inflection-js](http://code.google.com/p/inflection-js/)
167 | - [Javascript Rails-like inflector](http://snippets.dzone.com/posts/show/3205)
168 |
169 | Though no code was taken directly from them, they deserve plenty of props for doing it before me. If underscore isn't your thing, check them out!
170 |
171 | ## Contributors
172 |
173 | ```
174 | 66 Jeremy Ruppel
175 | 7 Landon Schropp
176 | 2 Johnathon Sanders
177 | 2 Seggy Umboh
178 | 1 Sam Dornan
179 | 1 Shane Riley
180 | 1 bramski
181 | 1 maratfakhreev
182 | 1 Daniel Perez
183 | 1 trevor
184 | 1 Dayton Nolan
185 | 1 Joseph Spens
186 | 1 Kris Neuharth
187 | ```
188 |
189 | ## License
190 |
191 | [MIT License][LICENSE]
192 |
193 | [npm]: http://badge.fury.io/js/underscore.inflection
194 | [npm-badge]: https://badge.fury.io/js/underscore.inflection.svg
195 | [travis-ci]: https://travis-ci.org/jeremyruppel/underscore.inflection
196 | [travis-badge]: https://travis-ci.org/jeremyruppel/underscore.inflection.svg?branch=master
197 | [coveralls]: https://coveralls.io/r/jeremyruppel/underscore.inflection?branch=master
198 | [coveralls-badge]: https://img.shields.io/coveralls/jeremyruppel/underscore.inflection.svg
199 | [rails]: https://github.com/rails/rails
200 | [activesupport]: https://github.com/rails/rails/tree/master/activesupport
201 | [underscore]: http://documentcloud.github.com/underscore/
202 | [documentcloud]: http://www.documentcloud.org/home
203 | [LICENSE]: https://github.com/jeremyruppel/underscore.inflection/blob/master/LICENSE
204 |
--------------------------------------------------------------------------------
/lib/underscore.inflection.js:
--------------------------------------------------------------------------------
1 | // Underscore.inflection.js
2 | // (c) 2014 Jeremy Ruppel
3 | // Underscore.inflection is freely distributable under the MIT license.
4 | // Portions of Underscore.inflection are inspired or borrowed from ActiveSupport
5 | // Version 1.0.0
6 |
7 | (function(root, factory) {
8 | if (typeof define === 'function' && define.amd) {
9 | // AMD. Register as an anonymous module.
10 | define(['underscore'], factory);
11 | } else if (typeof require === 'function' && typeof exports === 'object') {
12 | // CommonJS
13 | module.exports = factory(require('underscore'));
14 | } else {
15 | // Browser globals (root is window)
16 | factory(root._);
17 | }
18 | })(this, function(_, undefined) {
19 | var plurals = [];
20 | var singulars = [];
21 | var uncountables = [];
22 |
23 | /**
24 | * Inflector
25 | */
26 | var inflector = {
27 |
28 | /**
29 | * `gsub` is a method that is just slightly different than our
30 | * standard `String#replace`. The main differences are that it
31 | * matches globally every time, and if no substitution is made
32 | * it returns `null`. It accepts a string for `word` and
33 | * `replacement`, and `rule` can be either a string or a regex.
34 | */
35 | gsub: function(word, rule, replacement) {
36 | var pattern = new RegExp(rule.source || rule, 'gi');
37 |
38 | return pattern.test(word) ? word.replace(pattern, replacement) : null;
39 | },
40 |
41 | /**
42 | * `plural` creates a new pluralization rule for the inflector.
43 | * `rule` can be either a string or a regex.
44 | */
45 | plural: function(rule, replacement) {
46 | plurals.unshift([rule, replacement]);
47 | },
48 |
49 | /**
50 | * Pluralizes the string passed to it. It also can accept a
51 | * number as the second parameter. If a number is provided,
52 | * it will pluralize the word to match the number. Optionally,
53 | * you can pass `true` as a third parameter. If found, this
54 | * will include the count with the output.
55 | */
56 | pluralize: function(word, count, includeNumber) {
57 | var result;
58 |
59 | if (count !== undefined) {
60 | count = parseFloat(count);
61 | result = (count === 1) ? this.singularize(word) : this.pluralize(word);
62 | result = (includeNumber) ? [count, result].join(' ') : result;
63 | } else {
64 | if (_(uncountables).include(word)) {
65 | return word;
66 | }
67 |
68 | result = word;
69 |
70 | _(plurals).detect(function(rule) {
71 | var gsub = this.gsub(word, rule[0], rule[1]);
72 |
73 | return gsub ? (result = gsub) : false;
74 | },
75 | this);
76 | }
77 |
78 | return result;
79 | },
80 |
81 | /**
82 | * `singular` creates a new singularization rule for the
83 | * inflector. `rule` can be either a string or a regex.
84 | */
85 | singular: function(rule, replacement) {
86 | singulars.unshift([rule, replacement]);
87 | },
88 |
89 | /**
90 | * `singularize` returns the singular version of the plural
91 | * passed to it.
92 | */
93 | singularize: function(word) {
94 | if (_(uncountables).include(word)) {
95 | return word;
96 | }
97 |
98 | var result = word;
99 |
100 | _(singulars).detect(function(rule) {
101 | var gsub = this.gsub(word, rule[0], rule[1]);
102 |
103 | return gsub ? (result = gsub) : false;
104 | },
105 | this);
106 |
107 | return result;
108 | },
109 |
110 | /**
111 | * `irregular` is a shortcut method to create both a
112 | * pluralization and singularization rule for the word at
113 | * the same time. You must supply both the singular form
114 | * and the plural form as explicit strings.
115 | */
116 | irregular: function(singular, plural) {
117 | this.plural('\\b' + singular + '\\b', plural);
118 | this.singular('\\b' + plural + '\\b', singular);
119 | },
120 |
121 | /**
122 | * `uncountable` creates a new uncountable rule for `word`.
123 | * Uncountable words do not get pluralized or singularized.
124 | */
125 | uncountable: function(word) {
126 | uncountables.unshift(word);
127 | },
128 |
129 | /**
130 | * `ordinalize` adds an ordinal suffix to `number`.
131 | */
132 | ordinalize: function(number) {
133 | if (isNaN(number)) {
134 | return number;
135 | }
136 |
137 | number = number.toString();
138 | var lastDigit = number.slice(-1);
139 | var lastTwoDigits = number.slice(-2);
140 |
141 | if (lastTwoDigits === '11' || lastTwoDigits === '12' || lastTwoDigits === '13') {
142 | return number + 'th';
143 | }
144 |
145 | switch (lastDigit) {
146 | case '1':
147 | return number + 'st';
148 | case '2':
149 | return number + 'nd';
150 | case '3':
151 | return number + 'rd';
152 | default:
153 | return number + 'th';
154 | }
155 | },
156 |
157 | /**
158 | * `titleize` capitalizes the first letter of each word in
159 | * the string `words`. It preserves the existing whitespace.
160 | */
161 | titleize: function(words) {
162 | if (typeof words !== 'string') {
163 | return words;
164 | }
165 |
166 | return words.replace(/\S+/g, function(word) {
167 | return word.charAt(0).toUpperCase() + word.slice(1);
168 | });
169 | },
170 |
171 | /**
172 | * Resets the inflector's rules to their initial state,
173 | * clearing out any custom rules that have been added.
174 | */
175 | resetInflections: function() {
176 | plurals = [];
177 | singulars = [];
178 | uncountables = [];
179 |
180 | this.plural(/$/, 's');
181 | this.plural(/s$/, 's');
182 | this.plural(/(ax|test)is$/, '$1es');
183 | this.plural(/(octop|vir)us$/, '$1i');
184 | this.plural(/(octop|vir)i$/, '$1i');
185 | this.plural(/(alias|status)$/, '$1es');
186 | this.plural(/(bu)s$/, '$1ses');
187 | this.plural(/(buffal|tomat)o$/, '$1oes');
188 | this.plural(/([ti])um$/, '$1a');
189 | this.plural(/([ti])a$/, '$1a');
190 | this.plural(/sis$/, 'ses');
191 | this.plural(/(?:([^f])fe|([lr])?f)$/, '$1$2ves');
192 | this.plural(/(hive)$/, '$1s');
193 | this.plural(/([^aeiouy]|qu)y$/, '$1ies');
194 | this.plural(/(x|ch|ss|sh)$/, '$1es');
195 | this.plural(/(matr|vert|ind)(?:ix|ex)$/, '$1ices');
196 | this.plural(/([m|l])ouse$/, '$1ice');
197 | this.plural(/([m|l])ice$/, '$1ice');
198 | this.plural(/^(ox)$/, '$1en');
199 | this.plural(/^(oxen)$/, '$1');
200 | this.plural(/(quiz)$/, '$1zes');
201 |
202 | this.singular(/s$/, '');
203 | this.singular(/(n)ews$/, '$1ews');
204 | this.singular(/([ti])a$/, '$1um');
205 | this.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/, '$1$2sis');
206 | this.singular(/(^analy)ses$/, '$1sis');
207 | this.singular(/([^f])ves$/, '$1fe');
208 | this.singular(/(hive)s$/, '$1');
209 | this.singular(/(tive)s$/, '$1');
210 | this.singular(/([lr])ves$/, '$1f');
211 | this.singular(/([^aeiouy]|qu)ies$/, '$1y');
212 | this.singular(/(s)eries$/, '$1eries');
213 | this.singular(/(m)ovies$/, '$1ovie');
214 | this.singular(/(ss)$/, '$1');
215 | this.singular(/(x|ch|ss|sh)es$/, '$1');
216 | this.singular(/([m|l])ice$/, '$1ouse');
217 | this.singular(/(bus)es$/, '$1');
218 | this.singular(/(o)es$/, '$1');
219 | this.singular(/(shoe)s$/, '$1');
220 | this.singular(/(cris|ax|test)es$/, '$1is');
221 | this.singular(/(octop|vir)i$/, '$1us');
222 | this.singular(/(alias|status)es$/, '$1');
223 | this.singular(/^(ox)en/, '$1');
224 | this.singular(/(vert|ind)ices$/, '$1ex');
225 | this.singular(/(matr)ices$/, '$1ix');
226 | this.singular(/(quiz)zes$/, '$1');
227 | this.singular(/(database)s$/, '$1');
228 |
229 | this.irregular('person', 'people');
230 | this.irregular('man', 'men');
231 | this.irregular('child', 'children');
232 | this.irregular('sex', 'sexes');
233 | this.irregular('move', 'moves');
234 | this.irregular('cow', 'kine');
235 |
236 | this.uncountable('equipment');
237 | this.uncountable('information');
238 | this.uncountable('rice');
239 | this.uncountable('money');
240 | this.uncountable('species');
241 | this.uncountable('series');
242 | this.uncountable('fish');
243 | this.uncountable('sheep');
244 | this.uncountable('jeans');
245 |
246 | return this;
247 | }
248 | };
249 |
250 | /**
251 | * Underscore integration
252 | */
253 | _.mixin(inflector.resetInflections());
254 |
255 | return inflector;
256 | });
257 |
--------------------------------------------------------------------------------