Files
narya/src/as/com/threerings/util/StringUtil.as
T
Ray Greenwell e7dbda821d Removed pointless 8-char optimization.
Now it is more useful for this to hash identically as in Java.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5753 542714f4-19e9-0310-aa3c-eee0fc999fb1
2009-04-28 01:14:01 +00:00

701 lines
22 KiB
ActionScript

//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.util {
import com.threerings.util.env.Environment;
import flash.utils.ByteArray;
import flash.utils.Dictionary;
import flash.utils.getQualifiedClassName;
public class StringUtil
{
/**
* Compares two String values, returning -1, 0, or 1.
*/
public static function compare (s1 :String, s2 :String) :int
{
return (s1 > s2) ? 1 : (s1 == s2 ? 0 : -1);
}
/**
* Get a reasonable hash code for the specified String.
*/
public static function hashCode (str :String) :int
{
var code :int = 0;
if (str != null) {
// hash identically to Java's String.hashCode(), for various reasons
for (var ii :int = 0; ii < str.length; ii++) {
code = 31 * code + str.charCodeAt(ii);
}
}
return code;
}
/**
* Is the specified string null, empty, or does it contain only whitespace?
*/
public static function isBlank (str :String) :Boolean
{
return (str == null) || (str.search("\\S") == -1);
}
/**
* Return the specified String, or "" if it is null.
*/
public static function deNull (str :String) :String
{
return (str == null) ? "" : str;
}
/**
* Does the specified string end with the specified substring.
*/
public static function endsWith (str :String, substr :String) :Boolean
{
var startDex :int = str.length - substr.length;
return (startDex >= 0) && (str.indexOf(substr, startDex) >= 0);
}
/**
* Does the specified string start with the specified substring.
*/
public static function startsWith (str :String, substr :String) :Boolean
{
// just check once if it's at the beginning
return (str.lastIndexOf(substr, 0) == 0);
}
/**
* Return true iff the first character is a lower-case character.
*/
public static function isLowerCase (str :String) :Boolean
{
var firstChar :String = str.charAt(0);
return (firstChar.toUpperCase() != firstChar) &&
(firstChar.toLowerCase() == firstChar);
}
/**
* Return true iff the first character is an upper-case character.
*/
public static function isUpperCase (str :String) :Boolean
{
var firstChar :String = str.charAt(0);
return (firstChar.toUpperCase() == firstChar) &&
(firstChar.toLowerCase() != firstChar);
}
/**
* Parse an integer more anally than the built-in parseInt() function,
* throwing an ArgumentError if there are any invalid characters.
*
* The built-in parseInt() will ignore trailing non-integer characters.
*
* @param str The string to parse.
* @param radix The radix to use, from 2 to 16. If not specified the radix will be 10,
* unless the String begins with "0x" in which case it will be 16,
* or the String begins with "0" in which case it will be 8.
*/
public static function parseInteger (str :String, radix :uint = 0) :int
{
return int(parseInt0(str, radix, true));
}
/**
* Parse an integer more anally than the built-in parseInt() function,
* throwing an ArgumentError if there are any invalid characters.
*
* The built-in parseInt() will ignore trailing non-integer characters.
*
* @param str The string to parse.
* @param radix The radix to use, from 2 to 16. If not specified the radix will be 10,
* unless the String begins with "0x" in which case it will be 16,
* or the String begins with "0" in which case it will be 8.
*/
public static function parseUnsignedInteger (str :String, radix :uint = 0) :uint
{
var result :Number = parseInt0(str, radix, false);
if (result < 0) {
throw new ArgumentError("parseUnsignedInteger parsed negative value [value=" + str +
"].");
}
return uint(result);
}
/**
* Format the specified number, nicely, with commas.
* TODO: format specifyer, locale handling, etc. We'll probably move this into a
* NumberFormat-style class.
*/
public static function formatNumber (n :Number) :String
{
var postfix :String = "";
var s :String = n.toString(); // use standard to-stringing
// move any fractional portion to the postfix
const dex :int = s.lastIndexOf(".");
if (dex != -1) {
postfix = s.substring(dex);
s = s.substring(0, dex);
}
// hackily add commas
var prefixLength :int = (n < 0) ? 1 : 0;
while (s.length - prefixLength > 3) {
postfix = "," + s.substring(s.length - 3) + postfix;
s = s.substring(0, s.length - 3);
}
return s + postfix;
}
/**
* Parse a Number from a String, throwing an ArgumentError if there are any
* invalid characters.
*
* 1.5, 2e-3, -Infinity, Infinity, and NaN are all valid Strings.
*
* @param str the String to parse.
*/
public static function parseNumber (str :String) :Number
{
if (str == null) {
throw new ArgumentError("Cannot parseNumber(null)");
}
// deal with a few special cases
if (str == "Infinity") {
return Infinity;
} else if (str == "-Infinity") {
return -Infinity;
} else if (str == "NaN") {
return NaN;
}
const originalString :String = str;
str = str.replace(",", "");
const noCommas :String = str;
// validate all characters before the "e"
str = validateDecimal(str, true, true);
// validate all characters after the "e"
if (null != str && str.charAt(0) == "e") {
str = str.substring(1);
validateDecimal(str, false, false);
}
if (null == str) {
throw new ArgumentError("Could not convert '" + originalString + "' to Number");
}
// let Flash do the actual conversion
return parseFloat(noCommas);
}
/**
* Parse a Boolean from a String, throwing an ArgumentError if the String
* contains invalid characters.
*
* "1", "0", and any capitalization variation of "true" and "false" are
* the only valid input values.
*
* @param str the String to parse.
*/
public static function parseBoolean (str :String) :Boolean
{
var originalString :String = str;
if (str != null) {
str = str.toLowerCase();
if (str == "true" || str == "1") {
return true;
} else if (str == "false" || str == "0") {
return false;
}
}
throw new ArgumentError("Could not convert '" + originalString + "' to Boolean");
}
/**
* Append 0 or more copies of the padChar String to the input String
* until it is at least the specified length.
*/
public static function pad (
str :String, length :int, padChar :String = " ") :String
{
while (str.length < length) {
str += padChar;
}
return str;
}
/**
* Prepend 0 or more copies of the padChar String to the input String
* until it is at least the specified length.
*/
public static function prepad (
str :String, length :int, padChar :String = " ") :String
{
while (str.length < length) {
str = padChar + str;
}
return str;
}
/**
* Substitute "{n}" tokens for the corresponding passed-in arguments.
*/
public static function substitute (str :String, ... args) :String
{
// if someone passed an array as arg 1, fix it
args = Util.unfuckVarargs(args);
var len :int = args.length;
// TODO: FIXME: this might be wrong, if your {0} replacement has a {1} in it, then
// that'll get replaced next iteration.
for (var ii : int = 0; ii < len; ii++) {
str = str.replace(new RegExp("\\{" + ii + "\\}", "g"), args[ii]);
}
return str;
}
/**
* Utility function that strips whitespace from the beginning and end of a String.
*/
public static function trim (str :String) :String
{
return trimEnd(trimBeginning(str));
}
/**
* Utility function that strips whitespace from the beginning of a String.
*/
public static function trimBeginning (str :String) :String
{
if (str == null) {
return null;
}
var startIdx :int = 0;
// this works because charAt() with an invalid index returns "", which is not whitespace
while (isWhitespace(str.charAt(startIdx))) {
startIdx++;
}
// TODO: is this optimization necessary? It's possible that str.slice() does the same
// check and just returns 'str' if it's the full length
return (startIdx > 0) ? str.slice(startIdx, str.length) : str;
}
/**
* Utility function that strips whitespace from the end of a String.
*/
public static function trimEnd (str :String) :String
{
if (str == null) {
return null;
}
var endIdx :int = str.length;
// this works because charAt() with an invalid index returns "", which is not whitespace
while (isWhitespace(str.charAt(endIdx - 1))) {
endIdx--;
}
// TODO: is this optimization necessary? It's possible that str.slice() does the same
// check and just returns 'str' if it's the full length
return (endIdx < str.length) ? str.slice(0, endIdx) : str;
}
/**
* @return true if the specified String is == to a single whitespace character.
*/
public static function isWhitespace (character :String) :Boolean
{
switch (character) {
case " ":
case "\t":
case "\r":
case "\n":
case "\f":
return true;
default:
return false;
}
}
/**
* Nicely format the specified object into a String.
*/
public static function toString (obj :*, refs :Dictionary = null) :String
{
if (obj == null) { // checks null or undefined
return String(obj);
}
var isDictionary :Boolean = obj is Dictionary;
if (obj is Array || isDictionary || Util.isPlainObject(obj)) {
if (refs == null) {
refs = new Dictionary();
} else if (refs[obj] !== undefined) {
return "[cyclic reference]";
}
refs[obj] = true;
var s :String;
if (obj is Array) {
var arr :Array = (obj as Array);
s = "";
for (var ii :int = 0; ii < arr.length; ii++) {
if (ii > 0) {
s += ", ";
}
s += (ii + ": " + toString(arr[ii], refs));
}
return "Array(" + s + ")";
} else {
// TODO: maybe do this for any dynamic object? (would have to use describeType)
s = "";
for (var prop :String in obj) {
if (s.length > 0) {
s += ", ";
}
s += prop + "=>" + toString(obj[prop], refs);
}
return (isDictionary ? "Dictionary" : "Object") + "(" + s + ")";
}
} else if (obj is XML) {
return Util.XMLtoXMLString(obj as XML);
}
return String(obj);
}
/**
* Return a string containing all the public fields of the object
*
* @param buf the StringBuilder to which to append
* @param obj the object to be inspected
* @param fieldNames the names of fields to print, or null to print all.
*/
public static function fieldsToString (
buf :StringBuilder, obj :Object, fieldNames :Array = null) :void
{
if (fieldNames == null) {
fieldNames = Environment.enumerateFields(obj);
}
var appended :Boolean = false;
for each (var varName :String in fieldNames) {
if (appended) {
buf.append(", ");
}
buf.append(varName, "=", obj[varName]);
appended = true;
}
}
/**
* Return a pretty basic toString of the supplied Object.
*
* @param obj the object to be string'd
* @param fieldNames the names of fields to print, or null to print all.
*/
public static function simpleToString (obj :Object, fieldNames :Array = null) :String
{
var buf :StringBuilder = new StringBuilder("[");
buf.append(ClassUtil.tinyClassName(obj));
buf.append("(");
fieldsToString(buf, obj, fieldNames);
return buf.append(")]").toString();
}
/**
* Truncate the specified String if it is longer than maxLength.
* The string will be truncated at a position such that it is
* maxLength chars long after the addition of the 'append' String.
*
* @param append a String to add to the truncated String only after
* truncation.
*/
public static function truncate (
s :String, maxLength :int, append :String = "") :String
{
if ((s == null) || (s.length <= maxLength)) {
return s;
} else {
return s.substring(0, maxLength - append.length) + append;
}
}
/**
* Locate URLs in a string, return an array in which even elements
* are plain text, odd elements are urls (as Strings). Any even element
* may be an empty string.
*/
public static function parseURLs (s :String) :Array
{
var array :Array = [];
while (true) {
var result :Object = URL_REGEXP.exec(s);
if (result == null) {
break;
}
var index :int = int(result.index);
var url :String = String(result[0]);
array.push(s.substring(0, index), url);
s = s.substring(index + url.length);
}
if (s != "" || array.length == 0) { // avoid putting an empty string on the end
array.push(s);
}
return array;
}
/**
* Turn the specified byte array, containing only ascii characters, into a String.
*/
public static function fromBytes (bytes :ByteArray) :String
{
var s :String = "";
if (bytes != null) {
for (var ii :int = 0; ii < bytes.length; ii++) {
s += String.fromCharCode(bytes[ii]);
}
}
return s;
}
/**
* Turn the specified String, containing only ascii characters, into a ByteArray.
*/
public static function toBytes (s :String) :ByteArray
{
if (s == null) {
return null;
}
var ba :ByteArray = new ByteArray();
// if (true) {
for (var ii :int = 0; ii < s.length; ii++) {
ba[ii] = int(s.charCodeAt(ii)) & 0xFF;
}
// } else {
// ba.writeUTFBytes(s);
// }
return ba;
}
/**
* Generates a string from the supplied bytes that is the hex encoded
* representation of those byts. Returns the empty String for a
* <code>null</code> or empty byte array.
*/
public static function hexlate (bytes :ByteArray) :String
{
var str :String = "";
if (bytes != null) {
for (var ii :int = 0; ii < bytes.length; ii++) {
var b :int = bytes[ii];
str += HEX[b >> 4] + HEX[b & 0xF];
}
}
return str;
}
/**
* Turn a hexlated String back into a ByteArray.
*/
public static function unhexlate (hex :String) :ByteArray
{
if (hex == null || (hex.length % 2 != 0)) {
return null;
}
hex = hex.toLowerCase();
var data :ByteArray = new ByteArray();
for (var ii :int = 0; ii < hex.length; ii += 2) {
var value :int = HEX.indexOf(hex.charAt(ii)) << 4;
value += HEX.indexOf(hex.charAt(ii + 1));
// TODO: verify
// values over 127 are wrapped around, restoring negative bytes
data[ii / 2] = value;
}
return data;
}
/**
* Return a hexadecimal representation of an unsigned int, potentially left-padded with
* zeroes to arrive at of precisely the requested width, e.g.
* toHex(131, 4) -> "0083"
*/
public static function toHex (n :uint, width :uint) :String
{
return prepad(n.toString(16), width, "0");
}
/**
* Create line-by-line hexadecimal output with a counter, much like the
* 'hexdump' Unix utility. For debugging purposes.
*/
public static function hexdump (bytes :ByteArray) :String
{
var str :String = "";
for (var lineIx :int = 0; lineIx < bytes.length; lineIx += 16) {
str += toHex(lineIx, 4);
for (var byteIx :int = 0; byteIx < 16 && lineIx + byteIx < bytes.length; byteIx ++) {
var b :uint = bytes[lineIx + byteIx];
str += " " + HEX[b >> 4] + HEX[b & 0x0f];
}
str += "\n";
}
return str;
}
/**
* Internal helper function for parseNumber.
*/
protected static function validateDecimal (
str :String, allowDot :Boolean, allowTrailingE :Boolean) :String
{
// skip the leading minus
if (str.charAt(0) == "-") {
str = str.substring(1);
}
// validate that the characters in the string are all integers,
// with at most one '.'
var seenDot :Boolean;
var seenDigit :Boolean;
while (str.length > 0) {
var char :String = str.charAt(0);
if (char == ".") {
if (!allowDot || seenDot) {
return null;
}
seenDot = true;
} else if (char == "e") {
if (!allowTrailingE) {
return null;
}
break;
} else if (DECIMAL.indexOf(char) >= 0) {
seenDigit = true;
} else {
return null;
}
str = str.substring(1);
}
// ensure we've seen at least one digit so far
if (!seenDigit) {
return null;
}
return str;
}
/**
* Internal helper function for parseInteger and parseUnsignedInteger.
*/
protected static function parseInt0 (str :String, radix :uint, allowNegative :Boolean) :Number
{
if (str == null) {
throw new ArgumentError("Cannot parseInt(null)");
}
var negative :Boolean = (str.charAt(0) == "-");
if (negative) {
str = str.substring(1);
}
// handle this special case immediately, to prevent confusion about
// a leading 0 meaning "parse as octal"
if (str == "0") {
return 0;
}
if (radix == 0) {
if (startsWith(str, "0x")) {
str = str.substring(2);
radix = 16;
} else if (startsWith(str, "0")) {
str = str.substring(1);
radix = 8;
} else {
radix = 10;
}
} else if (radix == 16 && startsWith(str, "0x")) {
str = str.substring(2);
} else if (radix < 2 || radix > 16) {
throw new ArgumentError("Radix out of range: " + radix);
}
// now verify that str only contains valid chars for the radix
for (var ii :int = 0; ii < str.length; ii++) {
var dex :int = HEX.indexOf(str.charAt(ii).toLowerCase());
if (dex == -1 || dex >= radix) {
throw new ArgumentError("Invalid characters in String parseInt='" + arguments[0] +
"', radix=" + radix);
}
}
var result :Number = parseInt(str, radix);
if (isNaN(result)) {
// this shouldn't happen..
throw new ArgumentError("Could not parseInt=" + arguments[0]);
}
if (negative) {
result *= -1;
}
return result;
}
/** Hexidecimal digits. */
protected static const HEX :Array = [ "0", "1", "2", "3", "4",
"5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" ];
/** Decimal digits. */
protected static const DECIMAL :Array = [ "0", "1", "2", "3", "4",
"5", "6", "7", "8", "9" ];
/** A regular expression that finds URLs. */
protected static const URL_REGEXP :RegExp =
new RegExp("(http|https|ftp)://\\S+", "i");
}
}