API  0.9.7
 All Classes Files Functions Variables Macros Groups Pages
CPString.j
Go to the documentation of this file.
1 /*
2  * CPString.j
3  * Foundation
4  *
5  * Created by Francisco Tolmasky.
6  * Copyright 2008, 280 North, Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2.1 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21  */
22 
23 
24 @class CPException
25 @class CPURL
26 
28 @global CPRangeException
29 
35 CPCaseInsensitiveSearch = 1;
41 CPLiteralSearch = 2;
47 CPBackwardsSearch = 4;
52 CPAnchoredSearch = 8;
58 CPNumericSearch = 64;
65 
66 var CPStringUIDs = new CFMutableDictionary(),
67 
68  CPStringRegexSpecialCharacters = [
69  '/', '.', '*', '+', '?', '|', '$', '^',
70  '(', ')', '[', ']', '{', '}', '\\'
71  ],
72  CPStringRegexEscapeExpression = new RegExp("(\\" + CPStringRegexSpecialCharacters.join("|\\") + ")", 'g'),
73  CPStringRegexTrimWhitespace = new RegExp("(^\\s+|\\s+$)", 'g');
74 
88 @implementation CPString : CPObject
89 {
90  id __doxygen__;
91 }
92 
93 /*
94  @ignore
95 */
96 + (id)alloc
97 {
98  if ([self class] !== CPString)
99  return [super alloc];
100 
101  return new String;
102 }
103 
107 + (id)string
108 {
109  return [[self alloc] init];
110 }
111 
116 + (id)stringWithHash:(unsigned)aHash
117 {
118  var hashString = parseInt(aHash, 10).toString(16);
119  return "000000".substring(0, MAX(6 - hashString.length, 0)) + hashString;
120 }
121 
128 + (id)stringWithString:(CPString)aString
129 {
130  if (!aString)
131  [CPException raise:CPInvalidArgumentException
132  reason:"stringWithString: the string can't be 'nil'"];
133 
134  return [[self alloc] initWithString:aString];
135 }
136 
142 - (id)initWithString:(CPString)aString
143 {
144  if ([self class] === CPString)
145  return String(aString);
146 
147  var result = new String(aString);
148 
149  result.isa = [self class];
150 
151  return result;
152 }
153 
159 - (id)initWithFormat:(CPString)format, ...
160 {
161  if (!format)
162  [CPException raise:CPInvalidArgumentException
163  reason:"initWithFormat: the format can't be 'nil'"];
164 
165  self = ObjectiveJ.sprintf.apply(this, Array.prototype.slice.call(arguments, 2));
166  return self;
167 }
168 
175 + (id)stringWithFormat:(CPString)format, ...
176 {
177  if (!format)
178  [CPException raise:CPInvalidArgumentException
179  reason:"initWithFormat: the format can't be 'nil'"];
180 
181  return ObjectiveJ.sprintf.apply(this, Array.prototype.slice.call(arguments, 2));
182 }
183 
187 - (CPString)description
188 {
189  return self;
190 }
191 
195 - (int)length
196 {
197  return self.length;
198 }
199 
204 - (CPString)characterAtIndex:(CPUInteger)anIndex
205 {
206  return self.charAt(anIndex);
207 }
208 
209 // Combining strings
210 
217 - (CPString)stringByAppendingFormat:(CPString)format, ...
218 {
219  if (!format)
220  [CPException raise:CPInvalidArgumentException reason:"initWithFormat: the format can't be 'nil'"];
221 
222  return self + ObjectiveJ.sprintf.apply(this, Array.prototype.slice.call(arguments, 2));
223 }
224 
230 - (CPString)stringByAppendingString:(CPString)aString
231 {
232  return self + aString;
233 }
234 
247 - (CPString)stringByPaddingToLength:(unsigned)aLength withString:(CPString)aString startingAtIndex:(CPUInteger)anIndex
248 {
249  if (self.length == aLength)
250  return self;
251 
252  if (aLength < self.length)
253  return self.substr(0, aLength);
254 
255  var string = self,
256  substring = aString.substring(anIndex),
257  difference = aLength - self.length;
258 
259  while ((difference -= substring.length) >= 0)
260  string += substring;
261 
262  if (-difference < substring.length)
263  string += substring.substring(0, -difference);
264 
265  return string;
266 }
267 
268 //Dividing Strings
280 - (CPArray)componentsSeparatedByString:(CPString)aString
281 {
282  return self.split(aString);
283 }
284 
290 - (CPString)substringFromIndex:(unsigned)anIndex
291 {
292  return self.substr(anIndex);
293 }
294 
300 - (CPString)substringWithRange:(CPRange)aRange
301 {
302  if (aRange.location < 0 || CPMaxRange(aRange) > self.length)
303  [CPException raise:CPRangeException reason:"aRange out of bounds"];
304 
305  return self.substr(aRange.location, aRange.length);
306 }
307 
315 - (CPString)substringToIndex:(unsigned)anIndex
316 {
317  if (anIndex > self.length)
318  [CPException raise:CPRangeException reason:"index out of bounds"];
319 
320  return self.substring(0, anIndex);
321 }
322 
323 // Finding characters and substrings
324 
331 - (CPRange)rangeOfString:(CPString)aString
332 {
333  return [self rangeOfString:aString options:0];
334 }
335 
353 - (CPRange)rangeOfString:(CPString)aString options:(int)aMask
354 {
355  return [self rangeOfString:aString options:aMask range:nil];
356 }
357 
377 - (CPRange)rangeOfString:(CPString)aString options:(int)aMask range:(CPrange)aRange
378 {
379  // Searching for @"" always returns CPNotFound.
380  if (!aString)
381  return CPMakeRange(CPNotFound, 0);
382 
383  var string = (aRange == nil) ? self : [self substringWithRange:aRange],
384  location = CPNotFound;
385 
386  if (aMask & CPCaseInsensitiveSearch)
387  {
388  string = string.toLowerCase();
389  aString = aString.toLowerCase();
390  }
391 
392  if (aMask & CPBackwardsSearch)
393  {
394  location = string.lastIndexOf(aString);
395  if (aMask & CPAnchoredSearch && location + aString.length != string.length)
396  location = CPNotFound;
397  }
398  else if (aMask & CPAnchoredSearch)
399  location = string.substr(0, aString.length).indexOf(aString) != CPNotFound ? 0 : CPNotFound;
400  else
401  location = string.indexOf(aString);
402 
403  if (location == CPNotFound)
404  return CPMakeRange(CPNotFound, 0);
405 
406  return CPMakeRange(location + (aRange ? aRange.location : 0), aString.length);
407 }
408 
409 //Replacing Substrings
410 
411 - (CPString)stringByEscapingRegexControlCharacters
412 {
413  return self.replace(CPStringRegexEscapeExpression, "\\$1");
414 }
415 
423 - (CPString)stringByReplacingOccurrencesOfString:(CPString)target withString:(CPString)replacement
424 {
425  return self.replace(new RegExp([target stringByEscapingRegexControlCharacters], "g"), replacement);
426 }
427 
428 /*
429  Returns a new string in which all occurrences of a target string in a specified range of the receiver
430  are replaced by another given string.
431  @param target The string to replace
432  @param replacement the string with which to replace the \c target.
433  @param options A mask of options to use when comparing \c target with the receiver. Pass 0 to specify no options
434  @param searchRange The range in the receiver in which to search for \c target.
435 */
436 
437 - (CPString)stringByReplacingOccurrencesOfString:(CPString)target withString:(CPString)replacement options:(int)options range:(CPRange)searchRange
438 {
439  var start = self.substring(0, searchRange.location),
440  stringSegmentToSearch = self.substr(searchRange.location, searchRange.length),
441  end = self.substring(searchRange.location + searchRange.length, self.length),
442  target = [target stringByEscapingRegexControlCharacters],
443  regExp;
444 
445  if (options & CPCaseInsensitiveSearch)
446  regExp = new RegExp(target, "gi");
447  else
448  regExp = new RegExp(target, "g");
449 
450  return start + '' + stringSegmentToSearch.replace(regExp, replacement) + '' + end;
451 }
452 
453 /*
454  Returns a new string in which the characters in a specified range of the receiver
455  are replaced by a given string.
456  @param range A range of characters in the receiver.
457  @param replacement The string with which to replace the characters in \c range.
458 */
459 
460 - (CPString)stringByReplacingCharactersInRange:(CPRange)range withString:(CPString)replacement
461 {
462  return '' + self.substring(0, range.location) + replacement + self.substring(range.location + range.length, self.length);
463 }
464 
468 - (CPString)stringByTrimmingWhitespace
469 {
470  return self.replace(CPStringRegexTrimWhitespace, "");
471 }
472 
473 // Identifying and comparing strings
474 
480 - (CPComparisonResult)compare:(CPString)aString
481 {
482  return [self compare:aString options:nil];
483 }
484 
485 /*
486  Compares the receiver to the specified string.
487  @param aString the string with which to compare
488  @return the result of the comparison
489 */
490 - (CPComparisonResult)caseInsensitiveCompare:(CPString)aString
491 {
492  return [self compare:aString options:CPCaseInsensitiveSearch];
493 }
494 
495 // This is for speed
496 var CPStringNull = [CPNull null];
497 
504 - (CPComparisonResult)compare:(CPString)aString options:(int)aMask
505 {
506  if (aString === nil)
507  return CPOrderedDescending;
508 
509  if (aString === CPStringNull)
510  [CPException raise:CPInvalidArgumentException reason:"compare: argument can't be 'CPNull'"];
511 
512  var lhs = self,
513  rhs = aString;
514 
515  if (aMask & CPCaseInsensitiveSearch)
516  {
517  lhs = lhs.toLowerCase();
518  rhs = rhs.toLowerCase();
519  }
520 
521  if (aMask & CPDiacriticInsensitiveSearch)
522  {
523  lhs = lhs.stripDiacritics();
524  rhs = rhs.stripDiacritics();
525  }
526 
527  if (lhs < rhs)
528  return CPOrderedAscending;
529 
530  if (lhs > rhs)
531  return CPOrderedDescending;
532 
533  return CPOrderedSame;
534 }
535 
543 - (CPComparisonResult)compare:(CPString)aString options:(int)aMask range:(CPRange)range
544 {
545  var lhs = [self substringWithRange:range],
546  rhs = aString;
547 
548  return [lhs compare:rhs options:aMask];
549 }
550 
556 - (BOOL)hasPrefix:(CPString)aString
557 {
558  return aString && aString != "" && self.indexOf(aString) == 0;
559 }
560 
566 - (BOOL)hasSuffix:(CPString)aString
567 {
568  return aString && aString != "" && self.length >= aString.length && self.lastIndexOf(aString) == (self.length - aString.length);
569 }
570 
571 - (BOOL)isEqual:(id)anObject
572 {
573  if (self === anObject)
574  return YES;
575 
576  if (!anObject || ![anObject isKindOfClass:[CPString class]])
577  return NO;
578 
579  return [self isEqualToString:anObject];
580 }
581 
582 
586 - (BOOL)isEqualToString:(CPString)aString
587 {
588  return self == String(aString);
589 }
590 
594 - (CPString)UID
595 {
596  var UID = CPStringUIDs.valueForKey(self);
597 
598  if (!UID)
599  {
600  UID = objj_generateObjectUID();
601  CPStringUIDs.setValueForKey(self, UID);
602  }
603 
604  return UID + "";
605 }
606 
612 - (CPString)commonPrefixWithString:(CPString)aString
613 {
614  return [self commonPrefixWithString: aString options: 0];
615 }
616 
623 - (CPString)commonPrefixWithString:(CPString)aString options:(int)aMask
624 {
625  var len = 0, // length of common prefix
626  lhs = self,
627  rhs = aString,
628  min = MIN([lhs length], [rhs length]);
629 
630  if (aMask & CPCaseInsensitiveSearch)
631  {
632  lhs = [lhs lowercaseString];
633  rhs = [rhs lowercaseString];
634  }
635 
636  for (; len < min; len++)
637  {
638  if ([lhs characterAtIndex:len] !== [rhs characterAtIndex:len])
639  break;
640  }
641 
642  return [self substringToIndex:len];
643 }
644 
648 - (CPString)capitalizedString
649 {
650  var parts = self.split(/\b/g), // split on word boundaries
651  i = 0,
652  count = parts.length;
653 
654  for (; i < count; i++)
655  {
656  if (i == 0 || (/\s$/).test(parts[i - 1])) // only capitalize if previous token was whitespace
657  parts[i] = parts[i].substring(0, 1).toUpperCase() + parts[i].substring(1).toLowerCase();
658  else
659  parts[i] = parts[i].toLowerCase();
660  }
661  return parts.join("");
662 }
663 
667 - (CPString)lowercaseString
668 {
669  return self.toLowerCase();
670 }
671 
675 - (CPString)uppercaseString
676 {
677  return self.toUpperCase();
678 }
679 
683 - (double)doubleValue
684 {
685  return parseFloat(self, 10);
686 }
692 - (BOOL)boolValue
693 {
694  var replaceRegExp = new RegExp("^\\s*[\\+,\\-]?0*");
695  return RegExp("^[Y,y,t,T,1-9]").test(self.replace(replaceRegExp, ''));
696 }
697 
701 - (float)floatValue
702 {
703  return parseFloat(self, 10);
704 }
705 
709 - (int)intValue
710 {
711  return parseInt(self, 10);
712 }
713 
720 - (CPArray)pathComponents
721 {
722  if (self.length === 0)
723  return [""];
724 
725  if (self === "/")
726  return ["/"];
727 
728  var result = self.split('/');
729 
730  if (result[0] === "")
731  result[0] = "/";
732 
733  var index = result.length - 1;
734 
735  if (index > 0)
736  {
737  if (result[index] === "")
738  result[index] = "/";
739 
740  while (index--)
741  {
742  while (result[index] === "")
743  result.splice(index--, 1);
744  }
745  }
746 
747  return result;
748 }
749 
757 + (CPString)pathWithComponents:(CPArray)components
758 {
759  var size = components.length,
760  result = "",
761  i = -1,
762  firstRound = true,
763  firstIsSlash = false;
764 
765  while (++i < size)
766  {
767  var component = components[i],
768  lenMinusOne = component.length - 1;
769 
770  if (lenMinusOne >= 0 && (component !== "/" || firstRound)) // Skip "" and "/" (not first time)
771  {
772  if (lenMinusOne > 0 && component.indexOf("/",lenMinusOne) === lenMinusOne) // Ends with "/"
773  component = component.substring(0, lenMinusOne);
774 
775  if (firstRound)
776  {
777  if (component === "/")
778  firstIsSlash = true;
779  firstRound = false;
780  }
781  else if (!firstIsSlash)
782  result += "/";
783  else
784  firstIsSlash = false;
785 
786  result += component;
787  }
788  }
789  return result;
790 }
791 
797 - (CPString)pathExtension
798 {
799  if (self.lastIndexOf('.') === CPNotFound)
800  return "";
801 
802  return self.substr(self.lastIndexOf('.') + 1);
803 }
804 
810 - (CPString)lastPathComponent
811 {
812  var components = [self pathComponents],
813  lastIndex = components.length - 1,
814  lastComponent = components[lastIndex];
815 
816  return lastIndex > 0 && lastComponent === "/" ? components[lastIndex - 1] : lastComponent;
817 }
818 
825 - (CPString)stringByAppendingPathComponent:(CPString)aString
826 {
827  var components = [self pathComponents],
828  addComponents = aString && aString !== "/" ? [aString pathComponents] : [];
829 
830  return [CPString pathWithComponents:components.concat(addComponents)];
831 }
832 
840 - (CPString)stringByAppendingPathExtension:(CPString)ext
841 {
842  if (ext.indexOf('/') >= 0 || self.length === 0 || self === "/") // Can't handle these
843  return self;
844 
845  var components = [self pathComponents],
846  last = components.length - 1;
847 
848  if (last > 0 && components[last] === "/")
849  components.splice(last--, 1);
850 
851  components[last] = components[last] + "." + ext;
852 
853  return [CPString pathWithComponents:components];
854 }
855 
862 - (CPString)stringByDeletingLastPathComponent
863 {
864  if (self.length === 0)
865  return "";
866  else if (self === "/")
867  return "/";
868 
869  var components = [self pathComponents],
870  last = components.length - 1;
871 
872  if (components[last] === "/")
873  last--;
874 
875  components.splice(last, components.length - last);
876 
877  return [CPString pathWithComponents:components];
878 }
879 
886 - (CPString)stringByDeletingPathExtension
887 {
888  var extension = [self pathExtension];
889 
890  if (extension === "")
891  return self;
892  else if (self.lastIndexOf('.') < 1)
893  return self;
894 
895  return self.substr(0, [self length] - (extension.length + 1));
896 }
897 
898 - (CPString)stringByStandardizingPath
899 {
900  // FIXME: Expand tildes etc. in CommonJS?
901  return [[CPURL URLWithString:self] absoluteString];
902 }
903 
904 @end
905 
906 
907 @implementation CPString (JSON)
908 
912 + (CPString)JSONFromObject:(JSObject)anObject
913 {
914  return JSON.stringify(anObject);
915 }
916 
920 - (JSObject)objectFromJSON
921 {
922  return JSON.parse(self);
923 }
924 
925 @end
926 
927 
928 @implementation CPString (UUID)
929 
933 + (CPString)UUID
934 {
935  var g = @"",
936  i = 0;
937 
938  for (; i < 32; i++)
939  g += FLOOR(RAND() * 0xF).toString(0xF);
940 
941  return g;
942 }
943 
944 @end
945 
946 
947 var diacritics = [[192,198],[224,230],[231,231],[232,235],[236,239],[242,246],[249,252]], // Basic Latin ; Latin-1 Supplement.
948  normalized = [65,97,99,101,105,111,117];
949 
950 String.prototype.stripDiacritics = function()
951 {
952  var output = "";
953 
954  for (var indexSource = 0; indexSource < this.length; indexSource++)
955  {
956  var code = this.charCodeAt(indexSource);
957 
958  for (var i = 0; i < diacritics.length; i++)
959  {
960  var drange = diacritics[i];
961 
962  if (code >= drange[0] && code <= drange[drange.length - 1])
963  {
964  code = normalized[i];
965  break;
966  }
967  }
968 
969  output += String.fromCharCode(code);
970  }
971 
972  return output;
973 };
974 
975 String.prototype.isa = CPString;