1 2 //////////////////////////////////////////////////////////////////////////////////////////// 3 // Copyright (c) 2012 Christopher Nicholson-Sauls // 4 // // 5 // Permission is hereby granted, free of charge, to any person obtaining a copy of this // 6 // software and associated documentation files (the "Software"), to deal in the // 7 // Software without restriction, including without limitation the rights to use, copy, // 8 // modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, // 9 // and to permit persons to whom the Software is furnished to do so, subject to the // 10 // following conditions: // 11 // // 12 // The above copyright notice and this permission notice shall be included in all // 13 // copies or substantial portions of the Software. // 14 // // 15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // 16 // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A // 17 // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // 18 // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF // 19 // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE // 20 // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // 21 //////////////////////////////////////////////////////////////////////////////////////////// 22 23 /** 24 * 25 */ 26 module zeal.inflector; 27 28 import std.algorithm; 29 import std.array; 30 import std.ascii; 31 import std.conv; 32 import std.range; 33 import std.string; 34 35 import zeal.config; 36 37 /** 38 * 39 */ 40 string camelize ( string src, bool upperFirst = true ) { 41 auto app = appender!string(); 42 bool beginning = upperFirst; 43 foreach ( char c; src ) { 44 if ( c == '_' ) { 45 beginning = true; 46 continue; 47 } 48 49 if ( c == '/' ) { 50 app.put( c ); 51 beginning = upperFirst; 52 } 53 else { 54 app.put( cast( char ) ( beginning ? toUpper( c ) : toLower( c ) ) ); 55 beginning = false; 56 } 57 } 58 return app.data; 59 } 60 unittest { 61 assert( "foo".camelize() == "Foo" ); 62 assert( "foo_bar".camelize() == "FooBar" ); 63 assert( "foo_bar".camelize( false ) == "fooBar" ); 64 assert( "foo/bar".camelize() == "Foo/Bar" ); 65 assert( "foo/bar".camelize( false ) == "foo/bar" ); 66 } 67 68 /** 69 * 70 */ 71 string childize ( string src ) { 72 auto offset = src.retro().countUntil( '.' ); 73 if ( offset == -1 ) { 74 return src; 75 } 76 return src[ $ - offset .. $ ]; 77 } 78 unittest { 79 assert( "foo".childize() == "foo" ); 80 assert( "foo.bar".childize() == "bar" ); 81 assert( "a.b.foo".childize() == "foo" ); 82 } 83 84 /** 85 * 86 */ 87 string classify ( string src ) { 88 return src.singularize().camelize(); 89 } 90 91 /** 92 * 93 */ 94 string controllerize ( string src ) { 95 return src.camelize() ~ "Controller"; 96 } 97 98 /** 99 * 100 */ 101 string dasherize ( string src ) { 102 return src.replace( "_", "-" ); 103 } 104 unittest { 105 assert( "foo".dasherize() == "foo" ); 106 assert( "foo_bar".dasherize() == "foo-bar" ); 107 } 108 109 /** 110 * 111 */ 112 string decamelize ( string src ) { 113 auto app = appender!string(); 114 size_t pos; 115 foreach ( i, char c; src ) { 116 if ( isUpper( c ) && i > 0 ) { 117 app.put( src[ pos .. i ].toLower() ); 118 if( src[ i - 1 ] != '/' ) { 119 app.put( "_" ); 120 } 121 pos = i; 122 } 123 } 124 app.put( src[ pos .. $ ].toLower() ); 125 return app.data; 126 } 127 unittest { 128 assert( "Foo".decamelize() == "foo" ); 129 assert( "FooBar".decamelize() == "foo_bar" ); 130 assert( "fooBar".decamelize() == "foo_bar" ); 131 assert( "Foo/Bar".decamelize() == "foo/bar" ); 132 assert( "foo/bar".decamelize() == "foo/bar" ); 133 } 134 135 /** 136 * 137 */ 138 string foreignKey ( string src, string sep = "_" ) { 139 return src.modulize().toLower().singularize() ~ sep ~ "id"; 140 } 141 142 /** 143 * 144 */ 145 string humanize ( string src ) { 146 auto atoms = src.split( "_" ); 147 if ( atoms[ $ - 1 ] == "id" ) { 148 atoms.length -= 1; 149 } 150 atoms[ 0 ] = atoms[ 0 ].capitalize(); 151 return atoms.join( " " ); 152 } 153 unittest { 154 assert( "foo".humanize() == "Foo" ); 155 assert( "foo_bar".humanize() == "Foo bar" ); 156 assert( "foo_bar_id".humanize() == "Foo bar" ); 157 } 158 159 /** 160 * 161 */ 162 string modulize ( string src ) { 163 auto offset = src.retro().countUntil( '/' ); 164 if ( offset == -1 ) { 165 return src; 166 } 167 return src[ $ - offset .. $ ]; 168 } 169 unittest { 170 assert( "foo".modulize() == "foo" ); 171 assert( "foo/bar".modulize() == "bar" ); 172 } 173 174 /** 175 * 176 */ 177 string ordinalize ( long n ) { 178 immutable SUFF = [ "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th" ]; 179 180 auto result = to!string( n ); 181 result ~= SUFF[ result[ $ - 1 ] - '0' ]; 182 return result; 183 } 184 unittest { 185 assert( ordinalize( 0 ) == "0th" ); 186 assert( ordinalize( 1 ) == "1st" ); 187 assert( ordinalize( 2 ) == "2nd" ); 188 assert( ordinalize( 3 ) == "3rd" ); 189 assert( ordinalize( 4 ) == "4th" ); 190 assert( ordinalize( 5 ) == "5th" ); 191 assert( ordinalize( 10 ) == "10th" ); 192 assert( ordinalize( 21 ) == "21st" ); 193 assert( ordinalize( 22 ) == "22nd" ); 194 assert( ordinalize( 23 ) == "23rd" ); 195 assert( ordinalize( 25 ) == "25th" ); 196 assert( ordinalize( 30 ) == "30th" ); 197 assert( ordinalize( 31 ) == "31st" ); 198 assert( ordinalize( 32 ) == "32nd" ); 199 assert( ordinalize( 33 ) == "33rd" ); 200 assert( ordinalize( 66 ) == "66th" ); 201 assert( ordinalize( 100 ) == "100th" ); 202 assert( ordinalize( 101 ) == "101st" ); 203 assert( ordinalize( 102 ) == "102nd" ); 204 assert( ordinalize( 103 ) == "103rd" ); 205 assert( ordinalize( 104 ) == "104th" ); 206 } 207 208 /** 209 * 210 */ 211 string packagize ( string src ) { 212 auto offset = 1 + src.retro().countUntil( '/' ); 213 return src[ 0 .. $ - offset ]; 214 } 215 unittest { 216 assert( "foo".packagize() == "foo" ); 217 assert( "foo/bar".packagize() == "foo" ); 218 } 219 220 /** 221 * 222 */ 223 string parentize ( string src ) { 224 auto offset = 1 + src.retro().countUntil( '.' ); 225 return src[ 0 .. $ - offset ]; 226 } 227 unittest { 228 assert( "foo".parentize() == "foo" ); 229 assert( "foo.bar".parentize() == "foo" ); 230 assert( "foo.a.b".parentize() == "foo.a" ); 231 } 232 233 234 /** 235 * 236 */ 237 string pluralize ( string src ) { 238 // first check custom inflections 239 alias ZealConfig!"inflections" custom; 240 for ( 241 size_t s = 0, p = 1; 242 p < custom.length; 243 s += 2, p += 2 244 ) { 245 if ( src == custom[ s ] ) { 246 return custom[ p ]; 247 } 248 } 249 250 // second check basic inflections 251 for ( 252 size_t s = 0, p = 1; 253 p < BASIC_INFLECTIONS.length; 254 s += 2, p += 2 255 ) { 256 if ( src.endsWith( BASIC_INFLECTIONS[ s ] ) ) { 257 string result = src[ 0 .. $ - BASIC_INFLECTIONS[ s ].length ]; 258 result ~= BASIC_INFLECTIONS[ p ]; 259 return result; 260 } 261 } 262 263 // if all else fails, just apply "s" 264 return src ~ "s"; 265 } 266 unittest { 267 assert( "thing".pluralize() == "things" ); 268 assert( "glass".pluralize() == "glasses" ); 269 assert( "dash".pluralize() == "dashes" ); 270 assert( "base".pluralize() == "bases" ); 271 assert( "box".pluralize() == "boxes" ); 272 assert( "zero".pluralize() == "zeroes" ); 273 assert( "woman".pluralize() == "women" ); 274 assert( "image".pluralize() == "images" ); 275 } 276 277 /** 278 * 279 */ 280 string singularize ( string src ) { 281 // first check custom inflections 282 alias ZealConfig!"inflections" custom; 283 for ( 284 size_t s = 0, p = 1; 285 p < custom.length; 286 s += 2, p += 2 287 ) { 288 if ( src == custom[ p ] ) { 289 return custom[ s ]; 290 } 291 } 292 293 // second check basic inflections 294 for ( 295 size_t s = 0, p = 1; 296 p < BASIC_INFLECTIONS.length; 297 s += 2, p += 2 298 ) { 299 if ( src.endsWith( BASIC_INFLECTIONS[ p ] ) ) { 300 string result = src[ 0 .. $ - BASIC_INFLECTIONS[ p ].length ]; 301 result ~= BASIC_INFLECTIONS[ s ]; 302 return result; 303 } 304 } 305 306 // if all else fails, just remove "s" if present 307 if ( src[ $ - 1 ] == 's' ) { 308 return src[ 0 .. $ - 1 ]; 309 } 310 return src; 311 } 312 unittest { 313 assert( "things".singularize() == "thing" ); 314 assert( "glasses".singularize() == "glass" ); 315 assert( "dashes".singularize() == "dash" ); 316 assert( "bases".singularize() == "base" ); 317 assert( "boxes".singularize() == "box" ); 318 assert( "zeroes".singularize() == "zero" ); 319 assert( "women".singularize() == "woman" ); 320 assert( "images".singularize() == "image" ); 321 } 322 323 //////////////////////////////////////////////////////////////////////////////////////////////////// 324 private: 325 //////////////////////////////////////////////////////////////////////////////////////////////////// 326 327 /** 328 * 329 */ 330 enum BASIC_INFLECTIONS = [ 331 "ss", "sses", 332 "sh", "shes", 333 "se", "ses", 334 "ox", "oxes", 335 "o", "oes", 336 "man", "men" 337 ];