API  0.9.8
 All Classes Files Functions Variables Typedefs 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 
26 @global CPRangeException
27 
57 
58 var CPStringUIDs = new CFMutableDictionary(),
59 
61  '/', '.', '*', '+', '?', '|', '$', '^',
62  '(', ')', '[', ']', '{', '}', '\\'
63  ],
64  CPStringRegexEscapeExpression = new RegExp("(\\" + CPStringRegexSpecialCharacters.join("|\\") + ")", 'g'),
65  CPStringRegexTrimWhitespace = new RegExp("(^\\s+|\\s+$)", 'g');
66 
79 @implementation CPString : CPObject
80 {
81  id __doxygen__;
82 }
83 
84 /*
85  @ignore
86 */
87 + (id)alloc
88 {
89  if ([self class] !== CPString)
90  return [super alloc];
91 
92  return new String;
93 }
94 
98 + (id)string
99 {
100  return [[self alloc] init];
101 }
102 
107 + (id)stringWithHash:(unsigned)aHash
108 {
109  var hashString = parseInt(aHash, 10).toString(16);
110  return "000000".substring(0, MAX(6 - hashString.length, 0)) + hashString;
111 }
112 
119 + (id)stringWithString:(CPString)aString
120 {
121  if (!aString)
122  [CPException raise:CPInvalidArgumentException
123  reason:"stringWithString: the string can't be 'nil'"];
124 
125  return [[self alloc] initWithString:aString];
126 }
127 
133 - (id)initWithString:(CPString)aString
134 {
135  if ([self class] === CPString)
136  return String(aString);
137 
138  var result = new String(aString);
139 
140  result.isa = [self class];
141 
142  return result;
143 }
144 
150 - (id)initWithFormat:(CPString)format, ...
151 {
152  if (!format)
153  [CPException raise:CPInvalidArgumentException
154  reason:"initWithFormat: the format can't be 'nil'"];
155 
156  self = ObjectiveJ.sprintf.apply(this, Array.prototype.slice.call(arguments, 2));
157  return self;
158 }
159 
166 + (id)stringWithFormat:(CPString)format, ...
167 {
168  if (!format)
169  [CPException raise:CPInvalidArgumentException
170  reason:"initWithFormat: the format can't be 'nil'"];
171 
172  return ObjectiveJ.sprintf.apply(this, Array.prototype.slice.call(arguments, 2));
173 }
174 
178 - (CPString)description
179 {
180  return self;
181 }
182 
186 - (int)length
187 {
188  return self.length;
189 }
190 
195 - (CPString)characterAtIndex:(CPUInteger)anIndex
196 {
197  return self.charAt(anIndex);
198 }
199 
200 // Combining strings
201 
208 - (CPString)stringByAppendingFormat:(CPString)format, ...
209 {
210  if (!format)
211  [CPException raise:CPInvalidArgumentException reason:"initWithFormat: the format can't be 'nil'"];
212 
213  return self + ObjectiveJ.sprintf.apply(this, Array.prototype.slice.call(arguments, 2));
214 }
215 
221 - (CPString)stringByAppendingString:(CPString)aString
222 {
223  return self + aString;
224 }
225 
238 - (CPString)stringByPaddingToLength:(unsigned)aLength withString:(CPString)aString startingAtIndex:(CPUInteger)anIndex
239 {
240  if (self.length == aLength)
241  return self;
242 
243  if (aLength < self.length)
244  return self.substr(0, aLength);
245 
246  var string = self,
247  substring = aString.substring(anIndex),
248  difference = aLength - self.length;
249 
250  while ((difference -= substring.length) >= 0)
251  string += substring;
252 
253  if (-difference < substring.length)
254  string += substring.substring(0, -difference);
255 
256  return string;
257 }
258 
259 //Dividing Strings
271 - (CPArray)componentsSeparatedByString:(CPString)aString
272 {
273  return self.split(aString);
274 }
275 
281 - (CPString)substringFromIndex:(unsigned)anIndex
282 {
283  return self.substr(anIndex);
284 }
285 
291 - (CPString)substringWithRange:(CPRange)aRange
292 {
293  if (aRange.location < 0 || CPMaxRange(aRange) > self.length)
294  [CPException raise:CPRangeException reason:"aRange out of bounds"];
295 
296  return self.substr(aRange.location, aRange.length);
297 }
298 
306 - (CPString)substringToIndex:(unsigned)anIndex
307 {
308  if (anIndex > self.length)
309  [CPException raise:CPRangeException reason:"index out of bounds"];
310 
311  return self.substring(0, anIndex);
312 }
313 
314 // Finding characters and substrings
315 
322 - (CPRange)rangeOfString:(CPString)aString
323 {
324  return [self rangeOfString:aString options:0];
325 }
326 
345 - (CPRange)rangeOfString:(CPString)aString options:(int)aMask
346 {
347  return [self rangeOfString:aString options:aMask range:nil];
348 }
349 
370 - (CPRange)rangeOfString:(CPString)aString options:(int)aMask range:(CPrange)aRange
371 {
372  // Searching for @"" always returns CPNotFound.
373  if (!aString)
374  return CPMakeRange(CPNotFound, 0);
375 
376  var string = (aRange == nil) ? self : [self substringWithRange:aRange],
377  location = CPNotFound;
378 
379  if (aMask & CPCaseInsensitiveSearch)
380  {
381  string = string.toLowerCase();
382  aString = aString.toLowerCase();
383  }
384  if (aMask & CPDiacriticInsensitiveSearch)
385  {
386  string = string.stripDiacritics();
387  aString = aString.stripDiacritics();
388  }
389 
390  if (aMask & CPBackwardsSearch)
391  {
392  location = string.lastIndexOf(aString);
393  if (aMask & CPAnchoredSearch && location + aString.length != string.length)
394  location = CPNotFound;
395  }
396  else if (aMask & CPAnchoredSearch)
397  location = string.substr(0, aString.length).indexOf(aString) != CPNotFound ? 0 : CPNotFound;
398  else
399  location = string.indexOf(aString);
400 
401  if (location == CPNotFound)
402  return CPMakeRange(CPNotFound, 0);
403 
404  return CPMakeRange(location + (aRange ? aRange.location : 0), aString.length);
405 }
406 
407 //Replacing Substrings
408 
409 - (CPString)stringByEscapingRegexControlCharacters
410 {
411  return self.replace(CPStringRegexEscapeExpression, "\\$1");
412 }
413 
421 - (CPString)stringByReplacingOccurrencesOfString:(CPString)target withString:(CPString)replacement
422 {
423  return self.replace(new RegExp([target stringByEscapingRegexControlCharacters], "g"), replacement);
424 }
425 
426 /*
427  Returns a new string in which all occurrences of a target string in a specified range of the receiver
428  are replaced by another given string.
429  @param target The string to replace
430  @param replacement the string with which to replace the \c target.
431  @param options A mask of options to use when comparing \c target with the receiver. Pass 0 to specify no options
432  @param searchRange The range in the receiver in which to search for \c target.
433 */
434 
435 - (CPString)stringByReplacingOccurrencesOfString:(CPString)target withString:(CPString)replacement options:(int)options range:(CPRange)searchRange
436 {
437  var start = self.substring(0, searchRange.location),
438  stringSegmentToSearch = self.substr(searchRange.location, searchRange.length),
439  end = self.substring(searchRange.location + searchRange.length, self.length),
440  target = [target stringByEscapingRegexControlCharacters],
441  regExp;
442 
443  if (options & CPCaseInsensitiveSearch)
444  regExp = new RegExp(target, "gi");
445  else
446  regExp = new RegExp(target, "g");
447 
448  return start + '' + stringSegmentToSearch.replace(regExp, replacement) + '' + end;
449 }
450 
451 /*
452  Returns a new string in which the characters in a specified range of the receiver
453  are replaced by a given string.
454  @param range A range of characters in the receiver.
455  @param replacement The string with which to replace the characters in \c range.
456 */
457 
458 - (CPString)stringByReplacingCharactersInRange:(CPRange)range withString:(CPString)replacement
459 {
460  return '' + self.substring(0, range.location) + replacement + self.substring(range.location + range.length, self.length);
461 }
462 
466 - (CPString)stringByTrimmingWhitespace
467 {
468  return self.replace(CPStringRegexTrimWhitespace, "");
469 }
470 
471 // Identifying and comparing strings
472 
478 - (CPComparisonResult)compare:(CPString)aString
479 {
480  return [self compare:aString options:nil];
481 }
482 
483 /*
484  Compares the receiver to the specified string.
485  @param aString the string with which to compare
486  @return the result of the comparison
487 */
488 - (CPComparisonResult)caseInsensitiveCompare:(CPString)aString
489 {
490  return [self compare:aString options:CPCaseInsensitiveSearch];
491 }
492 
493 // This is for speed
495 
502 - (CPComparisonResult)compare:(CPString)aString options:(int)aMask
503 {
504  if (aString === nil)
505  return CPOrderedDescending;
506 
507  if (aString === CPStringNull)
508  [CPException raise:CPInvalidArgumentException reason:"compare: argument can't be 'CPNull'"];
509 
510  var lhs = self,
511  rhs = aString;
512 
513  if (aMask & CPCaseInsensitiveSearch)
514  {
515  lhs = lhs.toLowerCase();
516  rhs = rhs.toLowerCase();
517  }
518 
519  if (aMask & CPDiacriticInsensitiveSearch)
520  {
521  lhs = lhs.stripDiacritics();
522  rhs = rhs.stripDiacritics();
523  }
524 
525  if (lhs < rhs)
526  return CPOrderedAscending;
527 
528  if (lhs > rhs)
529  return CPOrderedDescending;
530 
531  return CPOrderedSame;
532 }
533 
541 - (CPComparisonResult)compare:(CPString)aString options:(int)aMask range:(CPRange)range
542 {
543  var lhs = [self substringWithRange:range],
544  rhs = aString;
545 
546  return [lhs compare:rhs options:aMask];
547 }
548 
554 - (BOOL)hasPrefix:(CPString)aString
555 {
556  return aString && aString != "" && self.indexOf(aString) == 0;
557 }
558 
564 - (BOOL)hasSuffix:(CPString)aString
565 {
566  return aString && aString != "" && self.length >= aString.length && self.lastIndexOf(aString) == (self.length - aString.length);
567 }
568 
569 - (BOOL)isEqual:(id)anObject
570 {
571  if (self === anObject)
572  return YES;
573 
574  if (!anObject || ![anObject isKindOfClass:[CPString class]])
575  return NO;
576 
577  return [self isEqualToString:anObject];
578 }
579 
580 
584 - (BOOL)isEqualToString:(CPString)aString
585 {
586  return self == String(aString);
587 }
588 
592 - (CPString)UID
593 {
594  var UID = CPStringUIDs.valueForKey(self);
595 
596  if (!UID)
597  {
598  UID = objj_generateObjectUID();
599  CPStringUIDs.setValueForKey(self, UID);
600  }
601 
602  return UID + "";
603 }
604 
610 - (CPString)commonPrefixWithString:(CPString)aString
611 {
612  return [self commonPrefixWithString: aString options: 0];
613 }
614 
621 - (CPString)commonPrefixWithString:(CPString)aString options:(int)aMask
622 {
623  var len = 0, // length of common prefix
624  lhs = self,
625  rhs = aString,
626  min = MIN([lhs length], [rhs length]);
627 
628  if (aMask & CPCaseInsensitiveSearch)
629  {
630  lhs = [lhs lowercaseString];
631  rhs = [rhs lowercaseString];
632  }
633 
634  for (; len < min; len++)
635  {
636  if ([lhs characterAtIndex:len] !== [rhs characterAtIndex:len])
637  break;
638  }
639 
640  return [self substringToIndex:len];
641 }
642 
646 - (CPString)capitalizedString
647 {
648  var parts = self.split(/\b/g), // split on word boundaries
649  i = 0,
650  count = parts.length;
651 
652  for (; i < count; i++)
653  {
654  if (i == 0 || (/\s$/).test(parts[i - 1])) // only capitalize if previous token was whitespace
655  parts[i] = parts[i].substring(0, 1).toUpperCase() + parts[i].substring(1).toLowerCase();
656  else
657  parts[i] = parts[i].toLowerCase();
658  }
659  return parts.join("");
660 }
661 
665 - (CPString)lowercaseString
666 {
667  return self.toLowerCase();
668 }
669 
673 - (CPString)uppercaseString
674 {
675  return self.toUpperCase();
676 }
677 
682 {
683  return self.stripDiacritics();
684 }
685 
689 - (double)doubleValue
690 {
691  return parseFloat(self, 10);
692 }
698 - (BOOL)boolValue
699 {
700  var replaceRegExp = new RegExp("^\\s*[\\+,\\-]?0*");
701  return RegExp("^[Y,y,t,T,1-9]").test(self.replace(replaceRegExp, ''));
702 }
703 
707 - (float)floatValue
708 {
709  return parseFloat(self, 10);
710 }
711 
715 - (int)intValue
716 {
717  return parseInt(self, 10);
718 }
719 
726 - (CPArray)pathComponents
727 {
728  if (self.length === 0)
729  return [""];
730 
731  if (self === "/")
732  return ["/"];
733 
734  var result = self.split('/');
735 
736  if (result[0] === "")
737  result[0] = "/";
738 
739  var index = result.length - 1;
740 
741  if (index > 0)
742  {
743  if (result[index] === "")
744  result[index] = "/";
745 
746  while (index--)
747  {
748  while (result[index] === "")
749  result.splice(index--, 1);
750  }
751  }
752 
753  return result;
754 }
755 
763 + (CPString)pathWithComponents:(CPArray)components
764 {
765  var size = components.length,
766  result = "",
767  i = -1,
768  firstRound = true,
769  firstIsSlash = false;
770 
771  while (++i < size)
772  {
773  var component = components[i],
774  lenMinusOne = component.length - 1;
775 
776  if (lenMinusOne >= 0 && (component !== "/" || firstRound)) // Skip "" and "/" (not first time)
777  {
778  if (lenMinusOne > 0 && component.indexOf("/",lenMinusOne) === lenMinusOne) // Ends with "/"
779  component = component.substring(0, lenMinusOne);
780 
781  if (firstRound)
782  {
783  if (component === "/")
784  firstIsSlash = true;
785  firstRound = false;
786  }
787  else if (!firstIsSlash)
788  result += "/";
789  else
790  firstIsSlash = false;
791 
792  result += component;
793  }
794  }
795  return result;
796 }
797 
803 - (CPString)pathExtension
804 {
805  if (self.lastIndexOf('.') === CPNotFound)
806  return "";
807 
808  return self.substr(self.lastIndexOf('.') + 1);
809 }
810 
816 - (CPString)lastPathComponent
817 {
818  var components = [self pathComponents],
819  lastIndex = components.length - 1,
820  lastComponent = components[lastIndex];
821 
822  return lastIndex > 0 && lastComponent === "/" ? components[lastIndex - 1] : lastComponent;
823 }
824 
831 - (CPString)stringByAppendingPathComponent:(CPString)aString
832 {
833  var components = [self pathComponents],
834  addComponents = aString && aString !== "/" ? [aString pathComponents] : [];
835 
836  return [CPString pathWithComponents:components.concat(addComponents)];
837 }
838 
846 - (CPString)stringByAppendingPathExtension:(CPString)ext
847 {
848  if (ext.indexOf('/') >= 0 || self.length === 0 || self === "/") // Can't handle these
849  return self;
850 
851  var components = [self pathComponents],
852  last = components.length - 1;
853 
854  if (last > 0 && components[last] === "/")
855  components.splice(last--, 1);
856 
857  components[last] = components[last] + "." + ext;
858 
859  return [CPString pathWithComponents:components];
860 }
861 
868 - (CPString)stringByDeletingLastPathComponent
869 {
870  if (self.length === 0)
871  return "";
872  else if (self === "/")
873  return "/";
874 
875  var components = [self pathComponents],
876  last = components.length - 1;
877 
878  if (components[last] === "/")
879  last--;
880 
881  components.splice(last, components.length - last);
882 
883  return [CPString pathWithComponents:components];
884 }
885 
892 - (CPString)stringByDeletingPathExtension
893 {
894  var extension = [self pathExtension];
895 
896  if (extension === "")
897  return self;
898  else if (self.lastIndexOf('.') < 1)
899  return self;
900 
901  return self.substr(0, [self length] - (extension.length + 1));
902 }
903 
904 - (CPString)stringByStandardizingPath
905 {
906  // FIXME: Expand tildes etc. in CommonJS?
907  return [[CPURL URLWithString:self] absoluteString];
908 }
909 
910 @end
911 
912 
913 @implementation CPString (JSON)
914 
918 + (CPString)JSONFromObject:(JSObject)anObject
919 {
920  return JSON.stringify(anObject);
921 }
922 
926 - (JSObject)objectFromJSON
927 {
928  return JSON.parse(self);
929 }
930 
931 @end
932 
933 
934 @implementation CPString (UUID)
935 
939 + (CPString)UUID
940 {
941  var g = @"",
942  i = 0;
943 
944  for (; i < 32; i++)
945  g += FLOOR(RAND() * 0xF).toString(0xF);
946 
947  return g;
948 }
949 
950 @end
951 
952 var diacritics = [[192,198],[200,203],[204,207],[210,214],[217,220],[224,230],
953  [231,231],[232,235],[236,239],[242,246],[249,252]], // Basic Latin ; Latin-1 Supplement.
954  normalized = [65,69,73,79,85,97,99,101,105,111,117];
955 
956 String.prototype.stripDiacritics = function()
957 {
958  var output = "";
959 
960  for (var indexSource = 0; indexSource < this.length; indexSource++)
961  {
962  var code = this.charCodeAt(indexSource);
963 
964  for (var i = 0; i < diacritics.length; i++)
965  {
966  var drange = diacritics[i];
967 
968  if (code >= drange[0] && code <= drange[drange.length - 1])
969  {
970  code = normalized[i];
971  break;
972  }
973  }
974 
975  output += String.fromCharCode(code);
976  }
977 
978  return output;
979 };
980 
981 String.prototype.isa = CPString;