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 ];