├── .gitignore ├── .jscsrc ├── .tm_properties ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── bower.json ├── gulpfile.js ├── index.js ├── lib └── underscore.inflection.js ├── package-lock.json ├── package.json ├── script └── hook.sh └── test ├── gsub.js ├── index.html ├── inflector.js ├── irregular.js ├── ordinalize.js ├── plural.js ├── pluralize.js ├── setup.js ├── singular.js ├── singularize.js ├── titleize.js └── uncountable.js /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = process.env.JSCOV 2 | ? require('./lib-cov') 3 | : require('./lib/underscore.inflection.js'); 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------