API  1.0.0
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 
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 
723 - (int)integerValue
724 {
725  return parseInt(self, 10);
726 }
727 
734 - (CPArray)pathComponents
735 {
736  if (self.length === 0)
737  return [""];
738 
739  if (self === "/")
740  return ["/"];
741 
742  var result = self.split('/');
743 
744  if (result[0] === "")
745  result[0] = "/";
746 
747  var index = result.length - 1;
748 
749  if (index > 0)
750  {
751  if (result[index] === "")
752  result[index] = "/";
753 
754  while (index--)
755  {
756  while (result[index] === "")
757  result.splice(index--, 1);
758  }
759  }
760 
761  return result;
762 }
763 
771 + (CPString)pathWithComponents:(CPArray)components
772 {
773  var size = components.length,
774  result = "",
775  i = -1,
776  firstRound = true,
777  firstIsSlash = false;
778 
779  while (++i < size)
780  {
781  var component = components[i],
782  lenMinusOne = component.length - 1;
783 
784  if (lenMinusOne >= 0 && (component !== "/" || firstRound)) // Skip "" and "/" (not first time)
785  {
786  if (lenMinusOne > 0 && component.indexOf("/",lenMinusOne) === lenMinusOne) // Ends with "/"
787  component = component.substring(0, lenMinusOne);
788 
789  if (firstRound)
790  {
791  if (component === "/")
792  firstIsSlash = true;
793  firstRound = false;
794  }
795  else if (!firstIsSlash)
796  result += "/";
797  else
798  firstIsSlash = false;
799 
800  result += component;
801  }
802  }
803  return result;
804 }
805 
811 - (CPString)pathExtension
812 {
813  if (self.lastIndexOf('.') === CPNotFound)
814  return "";
815 
816  return self.substr(self.lastIndexOf('.') + 1);
817 }
818 
824 - (CPString)lastPathComponent
825 {
826  var components = [self pathComponents],
827  lastIndex = components.length - 1,
828  lastComponent = components[lastIndex];
829 
830  return lastIndex > 0 && lastComponent === "/" ? components[lastIndex - 1] : lastComponent;
831 }
832 
839 - (CPString)stringByAppendingPathComponent:(CPString)aString
840 {
841  var components = [self pathComponents],
842  addComponents = aString && aString !== "/" ? [aString pathComponents] : [];
843 
844  return [CPString pathWithComponents:components.concat(addComponents)];
845 }
846 
854 - (CPString)stringByAppendingPathExtension:(CPString)ext
855 {
856  if (ext.indexOf('/') >= 0 || self.length === 0 || self === "/") // Can't handle these
857  return self;
858 
859  var components = [self pathComponents],
860  last = components.length - 1;
861 
862  if (last > 0 && components[last] === "/")
863  components.splice(last--, 1);
864 
865  components[last] = components[last] + "." + ext;
866 
867  return [CPString pathWithComponents:components];
868 }
869 
876 - (CPString)stringByDeletingLastPathComponent
877 {
878  if (self.length === 0)
879  return "";
880  else if (self === "/")
881  return "/";
882 
883  var components = [self pathComponents],
884  last = components.length - 1;
885 
886  if (components[last] === "/")
887  last--;
888 
889  components.splice(last, components.length - last);
890 
891  return [CPString pathWithComponents:components];
892 }
893 
900 - (CPString)stringByDeletingPathExtension
901 {
902  var extension = [self pathExtension];
903 
904  if (extension === "")
905  return self;
906  else if (self.lastIndexOf('.') < 1)
907  return self;
908 
909  return self.substr(0, [self length] - (extension.length + 1));
910 }
911 
912 - (CPString)stringByStandardizingPath
913 {
914  // FIXME: Expand tildes etc. in CommonJS?
915  return [[CPURL URLWithString:self] absoluteString];
916 }
917 
918 @end
919 
920 
921 @implementation CPString (JSON)
922 
926 + (CPString)JSONFromObject:(JSObject)anObject
927 {
928  return JSON.stringify(anObject);
929 }
930 
934 - (JSObject)objectFromJSON
935 {
936  return JSON.parse(self);
937 }
938 
939 @end
940 
941 
942 @implementation CPString (UUID)
943 
947 + (CPString)UUID
948 {
949  var g = @"",
950  i = 0;
951 
952  for (; i < 32; i++)
953  g += FLOOR(RAND() * 0xF).toString(0xF);
954 
955  return g;
956 }
957 
958 @end
959 
960 var diacritics = [[192,198],[200,203],[204,207],[210,214],[217,220],[224,230],
961  [231,231],[232,235],[236,239],[242,246],[249,252]], // Basic Latin ; Latin-1 Supplement.
962  normalized = [65,69,73,79,85,97,99,101,105,111,117];
963 
964 String.prototype.stripDiacritics = function()
965 {
966  var output = "";
967 
968  for (var indexSource = 0; indexSource < this.length; indexSource++)
969  {
970  var code = this.charCodeAt(indexSource);
971 
972  for (var i = 0; i < diacritics.length; i++)
973  {
974  var drange = diacritics[i];
975 
976  if (code >= drange[0] && code <= drange[drange.length - 1])
977  {
978  code = normalized[i];
979  break;
980  }
981  }
982 
983  output += String.fromCharCode(code);
984  }
985 
986  return output;
987 };
988 
989 String.prototype.isa = CPString;
Used to implement exception handling (creating & raising).
Definition: CPException.h:2
CPDiacriticInsensitiveSearch
Definition: CPString.j:56
CPOrderedAscending
Definition: CPObjJRuntime.j:48
CPLiteralSearch
Definition: CPString.j:37
var isEqual
An object representation of nil.
Definition: CPNull.h:2
CPOrderedSame
Definition: CPObjJRuntime.j:54
var CPStringRegexTrimWhitespace
Definition: CPString.j:65
CPAnchoredSearch
Definition: CPString.j:46
CPString substringWithRange:(CPRange aRange)
Definition: CPString.j:291
CPString commonPrefixWithString:options:(CPString aString, [options] int aMask)
Definition: CPString.j:621
void raise:reason:(CPString aName, [reason] CPString aReason)
Definition: CPException.j:66
var diacritics
Definition: CPString.j:960
String prototype stripDiacritics
Definition: CPString.j:964
CPInvalidArgumentException
Definition: CPException.j:25
var CPStringNull
Definition: CPString.j:494
CPRange rangeOfString:options:(CPString aString, [options] int aMask)
Definition: CPString.j:345
function CPMaxRange(aRange)
Definition: CPRange.j:70
An immutable string (collection of characters).
Definition: CPString.h:2
CPNull null()
Definition: CPNull.j:51
CPString substringToIndex:(unsigned anIndex)
Definition: CPString.j:306
CPString absoluteString()
Definition: CPURL.j:105
CPOrderedDescending
Definition: CPObjJRuntime.j:60
CPRangeException
Definition: CPException.j:27
var CPStringRegexSpecialCharacters
Definition: CPString.j:60
CPNumericSearch
Definition: CPString.j:51
int length()
Definition: CPString.j:186
BOOL isEqualToString:(CPString aString)
Definition: CPString.j:584
id alloc()
Definition: CPString.j:87
var normalized
Definition: CPString.j:962
CPComparisonResult compare:options:(CPString aString, [options] int aMask)
Definition: CPString.j:502
var CPStringRegexEscapeExpression
Definition: CPString.j:64
CPBackwardsSearch
Definition: CPString.j:42
CPNotFound
Definition: CPObjJRuntime.j:62
global CPInvalidArgumentException global CPRangeException CPCaseInsensitiveSearch
Definition: CPString.j:32
CPString pathExtension()
Definition: CPString.j:811
CPRange rangeOfString:options:range:(CPString aString, [options] int aMask, [range] CPrange aRange)
Definition: CPString.j:370
CPString stripDiacritics()
Definition: CPString.j:681
FrameUpdater prototype start
Class class()
Definition: CPObject.j:179
id URLWithString:(CPString URLString)
Definition: CPURL.j:78
CPString stringByEscapingRegexControlCharacters()
Definition: CPString.j:409
Definition: CPURL.h:2
CPString pathWithComponents:(CPArray components)
Definition: CPString.j:771
CPRange function CPMakeRange(location, length)
Definition: CPRange.j:37
id alloc()
Definition: CPObject.j:130
CPArray pathComponents()
Definition: CPString.j:734
var CPStringUIDs
Definition: CPString.j:58
FrameUpdater prototype description