API  0.9.6
 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 #define _CPMaxRange(aRange) ((aRange).location + (aRange).length)
25 
61 
62 var CPStringUIDs = new CFMutableDictionary(),
63 
65  '/', '.', '*', '+', '?', '|', '$', '^',
66  '(', ')', '[', ']', '{', '}', '\\'
67  ],
68  CPStringRegexEscapeExpression = new RegExp("(\\" + CPStringRegexSpecialCharacters.join("|\\") + ")", 'g'),
69  CPStringRegexTrimWhitespace = new RegExp("(^\\s+|\\s+$)", 'g');
70 
84 @implementation CPString : CPObject
85 {
86  id __doxygen__;
87 }
88 
89 /*
90  @ignore
91 */
92 + (id)alloc
93 {
94  if ([self class] !== CPString)
95  return [super alloc];
96 
97  return new String;
98 }
99 
103 + (id)string
104 {
105  return [[self alloc] init];
106 }
107 
112 + (id)stringWithHash:(unsigned)aHash
113 {
114  var hashString = parseInt(aHash, 10).toString(16);
115  return "000000".substring(0, MAX(6 - hashString.length, 0)) + hashString;
116 }
117 
124 + (id)stringWithString:(CPString)aString
125 {
126  if (!aString)
127  [CPException raise:CPInvalidArgumentException
128  reason:"stringWithString: the string can't be 'nil'"];
129 
130  return [[self alloc] initWithString:aString];
131 }
132 
138 - (id)initWithString:(CPString)aString
139 {
140  if ([self class] === CPString)
141  return String(aString);
142 
143  var result = new String(aString);
144 
145  result.isa = [self class];
146 
147  return result;
148 }
149 
155 - (id)initWithFormat:(CPString)format, ...
156 {
157  if (!format)
158  [CPException raise:CPInvalidArgumentException
159  reason:"initWithFormat: the format can't be 'nil'"];
160 
161  self = ObjectiveJ.sprintf.apply(this, Array.prototype.slice.call(arguments, 2));
162  return self;
163 }
164 
171 + (id)stringWithFormat:(CPString)format, ...
172 {
173  if (!format)
174  [CPException raise:CPInvalidArgumentException
175  reason:"initWithFormat: the format can't be 'nil'"];
176 
177  return ObjectiveJ.sprintf.apply(this, Array.prototype.slice.call(arguments, 2));
178 }
179 
183 - (CPString)description
184 {
185  return self;
186 }
187 
191 - (int)length
192 {
193  return length;
194 }
195 
200 - (CPString)characterAtIndex:(unsigned)anIndex
201 {
202  return charAt(anIndex);
203 }
204 
205 // Combining strings
206 
213 - (CPString)stringByAppendingFormat:(CPString)format, ...
214 {
215  if (!format)
216  [CPException raise:CPInvalidArgumentException reason:"initWithFormat: the format can't be 'nil'"];
217 
218  return self + ObjectiveJ.sprintf.apply(this, Array.prototype.slice.call(arguments, 2));
219 }
220 
226 - (CPString)stringByAppendingString:(CPString)aString
227 {
228  return self + aString;
229 }
230 
243 - (CPString)stringByPaddingToLength:(unsigned)aLength withString:(CPString)aString startingAtIndex:(unsigned)anIndex
244 {
245  if (length == aLength)
246  return self;
247 
248  if (aLength < length)
249  return substr(0, aLength);
250 
251  var string = self,
252  substring = aString.substring(anIndex),
253  difference = aLength - length;
254 
255  while ((difference -= substring.length) >= 0)
256  string += substring;
257 
258  if (-difference < substring.length)
259  string += substring.substring(0, -difference);
260 
261  return string;
262 }
263 
264 //Dividing Strings
276 - (CPArray)componentsSeparatedByString:(CPString)aString
277 {
278  return split(aString);
279 }
280 
286 - (CPString)substringFromIndex:(unsigned)anIndex
287 {
288  return substr(anIndex);
289 }
290 
296 - (CPString)substringWithRange:(CPRange)aRange
297 {
298  if (aRange.location < 0 || _CPMaxRange(aRange) > length)
299  [CPException raise:CPRangeException reason:"aRange out of bounds"];
300 
301  return substr(aRange.location, aRange.length);
302 }
303 
311 - (CPString)substringToIndex:(unsigned)anIndex
312 {
313  if (anIndex > length)
314  [CPException raise:CPRangeException reason:"index out of bounds"];
315 
316  return substring(0, anIndex);
317 }
318 
319 // Finding characters and substrings
320 
327 - (CPRange)rangeOfString:(CPString)aString
328 {
329  return [self rangeOfString:aString options:0];
330 }
331 
349 - (CPRange)rangeOfString:(CPString)aString options:(int)aMask
350 {
351  return [self rangeOfString:aString options:aMask range:nil];
352 }
353 
373 - (CPRange)rangeOfString:(CPString)aString options:(int)aMask range:(CPrange)aRange
374 {
375  // Searching for @"" always returns CPNotFound.
376  if (!aString)
377  return CPMakeRange(CPNotFound, 0);
378 
379  var string = (aRange == nil) ? self : [self substringWithRange:aRange],
380  location = CPNotFound;
381 
382  if (aMask & CPCaseInsensitiveSearch)
383  {
384  string = string.toLowerCase();
385  aString = aString.toLowerCase();
386  }
387 
388  if (aMask & CPBackwardsSearch)
389  {
390  location = string.lastIndexOf(aString);
391  if (aMask & CPAnchoredSearch && location + aString.length != string.length)
392  location = CPNotFound;
393  }
394  else if (aMask & CPAnchoredSearch)
395  location = string.substr(0, aString.length).indexOf(aString) != CPNotFound ? 0 : CPNotFound;
396  else
397  location = string.indexOf(aString);
398 
399  if (location == CPNotFound)
400  return CPMakeRange(CPNotFound, 0);
401 
402  return CPMakeRange(location + (aRange ? aRange.location : 0), aString.length);
403 }
404 
405 //Replacing Substrings
406 
407 - (CPString)stringByEscapingRegexControlCharacters
408 {
409  return self.replace(CPStringRegexEscapeExpression, "\\$1");
410 }
411 
419 - (CPString)stringByReplacingOccurrencesOfString:(CPString)target withString:(CPString)replacement
420 {
421  return self.replace(new RegExp([target stringByEscapingRegexControlCharacters], "g"), replacement);
422 }
423 
424 /*
425  Returns a new string in which all occurrences of a target string in a specified range of the receiver
426  are replaced by another given string.
427  @param target The string to replace
428  @param replacement the string with which to replace the \c target.
429  @param options A mask of options to use when comparing \c target with the receiver. Pass 0 to specify no options
430  @param searchRange The range in the receiver in which to search for \c target.
431 */
432 
433 - (CPString)stringByReplacingOccurrencesOfString:(CPString)target withString:(CPString)replacement options:(int)options range:(CPRange)searchRange
434 {
435  var start = substring(0, searchRange.location),
436  stringSegmentToSearch = substr(searchRange.location, searchRange.length),
437  end = substring(searchRange.location + searchRange.length, self.length),
438  target = [target stringByEscapingRegexControlCharacters],
439  regExp;
440 
441  if (options & CPCaseInsensitiveSearch)
442  regExp = new RegExp(target, "gi");
443  else
444  regExp = new RegExp(target, "g");
445 
446  return start + '' + stringSegmentToSearch.replace(regExp, replacement) + '' + end;
447 }
448 
449 /*
450  Returns a new string in which the characters in a specified range of the receiver
451  are replaced by a given string.
452  @param range A range of characters in the receiver.
453  @param replacement The string with which to replace the characters in \c range.
454 */
455 
456 - (CPString)stringByReplacingCharactersInRange:(CPRange)range withString:(CPString)replacement
457 {
458  return '' + substring(0, range.location) + replacement + substring(range.location + range.length, self.length);
459 }
460 
464 - (CPString)stringByTrimmingWhitespace
465 {
466  return self.replace(CPStringRegexTrimWhitespace, "");
467 }
468 
469 // Identifying and comparing strings
470 
476 - (CPComparisonResult)compare:(CPString)aString
477 {
478  return [self compare:aString options:nil];
479 }
480 
481 /*
482  Compares the receiver to the specified string.
483  @param aString the string with which to compare
484  @return the result of the comparison
485 */
486 - (CPComparisonResult)caseInsensitiveCompare:(CPString)aString
487 {
488  return [self compare:aString options:CPCaseInsensitiveSearch];
489 }
490 
497 - (CPComparisonResult)compare:(CPString)aString options:(int)aMask
498 {
499  var lhs = self,
500  rhs = aString;
501 
502  if (aMask & CPCaseInsensitiveSearch)
503  {
504  lhs = lhs.toLowerCase();
505  rhs = rhs.toLowerCase();
506  }
507 
508  if (aMask & CPDiacriticInsensitiveSearch)
509  {
510  lhs = lhs.stripDiacritics();
511  rhs = rhs.stripDiacritics();
512  }
513 
514  if (lhs < rhs)
515  return CPOrderedAscending;
516 
517  if (lhs > rhs)
518  return CPOrderedDescending;
519 
520  return CPOrderedSame;
521 }
522 
530 - (CPComparisonResult)compare:(CPString)aString options:(int)aMask range:(CPRange)range
531 {
532  var lhs = [self substringWithRange:range],
533  rhs = aString;
534 
535  return [lhs compare:rhs options:aMask];
536 }
537 
543 - (BOOL)hasPrefix:(CPString)aString
544 {
545  return aString && aString != "" && indexOf(aString) == 0;
546 }
547 
553 - (BOOL)hasSuffix:(CPString)aString
554 {
555  return aString && aString != "" && length >= aString.length && lastIndexOf(aString) == (length - aString.length);
556 }
557 
558 - (BOOL)isEqual:(id)anObject
559 {
560  if (self === anObject)
561  return YES;
562 
563  if (!anObject || ![anObject isKindOfClass:[CPString class]])
564  return NO;
565 
566  return [self isEqualToString:anObject];
567 }
568 
569 
573 - (BOOL)isEqualToString:(CPString)aString
574 {
575  return self == String(aString);
576 }
577 
581 - (unsigned)UID
582 {
583  var UID = CPStringUIDs.valueForKey(self);
584 
585  if (!UID)
586  {
587  UID = objj_generateObjectUID();
588  CPStringUIDs.setValueForKey(self, UID);
589  }
590 
591  return UID + "";
592 }
593 
599 - (CPString)commonPrefixWithString:(CPString)aString
600 {
601  return [self commonPrefixWithString: aString options: 0];
602 }
603 
610 - (CPString)commonPrefixWithString:(CPString)aString options:(int)aMask
611 {
612  var len = 0, // length of common prefix
613  lhs = self,
614  rhs = aString,
615  min = MIN([lhs length], [rhs length]);
616 
617  if (aMask & CPCaseInsensitiveSearch)
618  {
619  lhs = [lhs lowercaseString];
620  rhs = [rhs lowercaseString];
621  }
622 
623  for (; len < min; len++)
624  {
625  if ([lhs characterAtIndex:len] !== [rhs characterAtIndex:len])
626  break;
627  }
628 
629  return [self substringToIndex:len];
630 }
631 
635 - (CPString)capitalizedString
636 {
637  var parts = self.split(/\b/g), // split on word boundaries
638  i = 0,
639  count = parts.length;
640 
641  for (; i < count; i++)
642  {
643  if (i == 0 || (/\s$/).test(parts[i - 1])) // only capitalize if previous token was whitespace
644  parts[i] = parts[i].substring(0, 1).toUpperCase() + parts[i].substring(1).toLowerCase();
645  else
646  parts[i] = parts[i].toLowerCase();
647  }
648  return parts.join("");
649 }
650 
654 - (CPString)lowercaseString
655 {
656  return toLowerCase();
657 }
658 
662 - (CPString)uppercaseString
663 {
664  return toUpperCase();
665 }
666 
670 - (double)doubleValue
671 {
672  return parseFloat(self, 10);
673 }
679 - (BOOL)boolValue
680 {
681  var replaceRegExp = new RegExp("^\\s*[\\+,\\-]?0*");
682  return RegExp("^[Y,y,t,T,1-9]").test(self.replace(replaceRegExp, ''));
683 }
684 
688 - (float)floatValue
689 {
690  return parseFloat(self, 10);
691 }
692 
696 - (int)intValue
697 {
698  return parseInt(self, 10);
699 }
700 
707 - (CPArray)pathComponents
708 {
709  if (length === 0)
710  return [""];
711 
712  if (self === "/")
713  return ["/"];
714 
715  var result = split('/');
716 
717  if (result[0] === "")
718  result[0] = "/";
719 
720  var index = result.length - 1;
721 
722  if (index > 0)
723  {
724  if (result[index] === "")
725  result[index] = "/";
726 
727  while (index--)
728  {
729  while (result[index] === "")
730  result.splice(index--, 1);
731  }
732  }
733 
734  return result;
735 }
736 
744 + (CPString)pathWithComponents:(CPArray)components
745 {
746  var size = components.length,
747  result = "",
748  i = -1,
749  firstRound = true,
750  firstIsSlash = false;
751 
752  while (++i < size)
753  {
754  var component = components[i],
755  lenMinusOne = component.length - 1;
756 
757  if (lenMinusOne >= 0 && (component !== "/" || firstRound)) // Skip "" and "/" (not first time)
758  {
759  if (lenMinusOne > 0 && component.indexOf("/",lenMinusOne) === lenMinusOne) // Ends with "/"
760  component = component.substring(0, lenMinusOne);
761 
762  if (firstRound)
763  {
764  if (component === "/")
765  firstIsSlash = true;
766  firstRound = false;
767  }
768  else if (!firstIsSlash)
769  result += "/";
770  else
771  firstIsSlash = false;
772 
773  result += component;
774  }
775  }
776  return result;
777 }
778 
784 - (CPString)pathExtension
785 {
786  if (lastIndexOf('.') === CPNotFound)
787  return "";
788 
789  return substr(lastIndexOf('.') + 1);
790 }
791 
797 - (CPString)lastPathComponent
798 {
799  var components = [self pathComponents],
800  lastIndex = components.length - 1,
801  lastComponent = components[lastIndex];
802 
803  return lastIndex > 0 && lastComponent === "/" ? components[lastIndex - 1] : lastComponent;
804 }
805 
812 - (CPString)stringByAppendingPathComponent:(CPString)aString
813 {
814  var components = [self pathComponents],
815  addComponents = aString && aString !== "/" ? [aString pathComponents] : [];
816 
817  return [CPString pathWithComponents:components.concat(addComponents)];
818 }
819 
827 - (CPString)stringByAppendingPathExtension:(CPString)ext
828 {
829  if (ext.indexOf('/') >= 0 || length === 0 || self === "/") // Can't handle these
830  return self;
831 
832  var components = [self pathComponents],
833  last = components.length - 1;
834 
835  if (last > 0 && components[last] === "/")
836  components.splice(last--, 1);
837 
838  components[last] = components[last] + "." + ext;
839 
840  return [CPString pathWithComponents:components];
841 }
842 
849 - (CPString)stringByDeletingLastPathComponent
850 {
851  if (length === 0)
852  return "";
853  else if (self === "/")
854  return "/";
855 
856  var components = [self pathComponents],
857  last = components.length - 1;
858 
859  if (components[last] === "/")
860  last--;
861 
862  components.splice(last, components.length - last);
863 
864  return [CPString pathWithComponents:components];
865 }
866 
873 - (CPString)stringByDeletingPathExtension
874 {
875  var extension = [self pathExtension];
876 
877  if (extension === "")
878  return self;
879  else if (lastIndexOf('.') < 1)
880  return self;
881 
882  return substr(0, [self length] - (extension.length + 1));
883 }
884 
885 - (CPString)stringByStandardizingPath
886 {
887  // FIXME: Expand tildes etc. in CommonJS?
888  return [[CPURL URLWithString:self] absoluteString];
889 }
890 
891 @end
892 
893 
894 @implementation CPString (JSON)
895 
899 + (CPString)JSONFromObject:(JSObject)anObject
900 {
901  return JSON.stringify(anObject);
902 }
903 
907 - (JSObject)objectFromJSON
908 {
909  return JSON.parse(self);
910 }
911 
912 @end
913 
914 
915 @implementation CPString (UUID)
916 
920 + (CPString)UUID
921 {
922  var g = @"",
923  i = 0;
924 
925  for (; i < 32; i++)
926  g += FLOOR(RAND() * 0xF).toString(0xF);
927 
928  return g;
929 }
930 
931 @end
932 
933 
934 var diacritics = [[192,198],[224,230],[231,231],[232,235],[236,239],[242,246],[249,252]], // Basic Latin ; Latin-1 Supplement.
935  normalized = [65,97,99,101,105,111,117];
936 
937 String.prototype.stripDiacritics = function()
938 {
939  var output = "";
940 
941  for (var indexSource = 0; indexSource < this.length; indexSource++)
942  {
943  var code = this.charCodeAt(indexSource);
944 
945  for (var i = 0; i < diacritics.length; i++)
946  {
947  var drange = diacritics[i];
948 
949  if (code >= drange[0] && code <= drange[drange.length - 1])
950  {
951  code = normalized[i];
952  break;
953  }
954  }
955 
956  output += String.fromCharCode(code);
957  }
958 
959  return output;
960 };
961 
962 String.prototype.isa = CPString;