home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Chip 2007 January, February, March & April
/
Chip-Cover-CD-2007-02.iso
/
boot
/
i386
/
root
/
usr
/
share
/
YaST2
/
modules
/
String.ycp
< prev
next >
Wrap
Text File
|
2006-11-29
|
28KB
|
1,117 lines
/**
* File: modules/String.ycp
* Package: yast2
* Summary: String manipulation routines
* Authors: Michal Svec <msvec@suse.cz>
*
* $Id: String.ycp 28838 2006-03-10 16:11:58Z mvidner $
*/
{
module "String";
textdomain "base";
/**
* Quote a string with 's
* @param var unquoted string
* @return quoted string
* @example quote("a'b") -> "a'\''b"
*/
global define string Quote(string var) ``{
if(var == nil || var == "") return "";
return mergestring(splitstring(var, "'"), "'\\''");
}
/**
* Unquote a string with 's (quoted with quote)
* @param var quoted string
* @return unquoted string
* @see quote
*/
global define string UnQuote(string var) ``{
if(var == nil || var == "") return "";
y2debug("var=%1", var);
while(regexpmatch(var, "'\\\\''")) {
var = regexpsub(var, "(.*)'\\\\''(.*)", "\\1'\\2");
y2debug("var=%1", var);
}
return var;
}
/**
* Optional formatted text
* @return sformat (f, s) if s is neither empty or nil, else ""
*/
global string OptFormat (string f, string s) {
return (s == "" || s == nil)? "": sformat (f, s);
}
/**
* Optional parenthesized text
* @return " (Foo)" if Foo is neither empty or nil, else ""
*/
global string OptParens (string s) {
return OptFormat (" (%1)", s);
}
/**
* @param l a list of strings
* @return only non-"" items
*/
global list<string> NonEmpty (list<string> l) {
return filter (string i, l, ``( i != "" ));
}
/**
* @param s \n-terminated items
* @return the items as a list, with empty lines removed
*/
global list<string> NewlineItems (string s) {
return NonEmpty (splitstring (s, "\n"));
}
/**
* Return a pretty description of a byte count
*
* Return a pretty description of a byte count with required precision
* and using KB, MB or GB as unit as appropriate.
*
* @param bytes size (e.g. free diskspace, memory size) in Bytes
* @param precision number of fraction digits in output
* @param omit_zeroes if true then do not add zeroes
* (usefull for memory size - 128 MB RAM looks better than 128.00 MB RAM)
* @return formatted string
*
* @example FormatSizeWithPrecision(4096, 2, true) -> "4 KB"
* @example FormatSizeWithPrecision(4096, 2, false) -> "4.00 KB"
*/
global define string FormatSizeWithPrecision(integer bytes, integer precision, boolean omit_zeroes) ``{
if(bytes == nil) return "";
string unit = "";
list units = [
/* KiloByte abbreviated */
_("KB"),
/* MegaByte abbreviated */
_("MB"),
/* GigaByte abbreviated */
_("GB"),
/* TeraByte abbreviated */
_("TB"),
];
integer index = 0;
float whole = tofloat(bytes);
while((whole > 1024.0 || whole < -1024.0) && index < size(units)) {
whole = whole / 1024.0;
unit = units[index]:"";
index = index + 1;
}
if (precision == nil || precision < 0 || (omit_zeroes == true && (whole - tointeger(whole) == 0.0)))
{
precision = 0;
}
return tostring(whole, precision) + " " + unit;
}
/**
* Return a pretty description of a byte count
*
* Return a pretty description of a byte count, with two fraction digits
* and using KB, MB or GB as unit as appropriate.
*
* @param bytes size (e.g. free diskspace) in Bytes
* @return formatted string
*
* @example FormatSize(23456767890) -> "223.70 MB"
*/
global define string FormatSize(integer bytes) ``{
return FormatSizeWithPrecision(bytes, 2, false);
}
/**
* Remove blanks at begin and end of input string.
* @param input string to be stripped
* @return stripped string
* @example CutBlanks(" any input ") -> "any input"
*/
global define string CutBlanks(string input) ``{
if(input == nil || size(input) < 1) return "";
integer pos1 = findfirstnotof(input, " \t");
if(pos1 == nil) return "";
integer pos2 = findlastnotof(input, " \t");
return substring(input, pos1, pos2 - pos1 + 1);
}
/**
* Remove any leading zeros
*
* Remove any leading zeros that make tointeger inadvertently
* assume an octal number (e.g. "09" -> "9", "0001" -> "1",
* but "0" -> "0")
*
* @param input number that might contain leadig zero
* @return string that has leading zeros removed
*/
global define string CutZeros(string input) ``{
if(input == nil || size(input) < 1) return "";
if(!regexpmatch(input, "^0.*")) return input;
string output = regexpsub(input, "^0+(.*)$", "\\1");
if(size(output) < 1) return "0";
return output;
}
/**
* Add spaces after the text to make it long enough
*
* Add spaces after the text to make it long enough. If the text is longer
* than requested, no changes are made.
*
* @param text text to be padded
* @param length requested length
* @return padded text
*/
global define string Pad(string text, integer length) {
if(text == nil) text = "";
integer rest = length - size(text);
string pad = "";
while(rest > 0) {
pad = pad + " ";
rest = rest - 1;
}
return text + pad;
}
/**
* Add zeros before the text to make it long enough.
*
* Add zeros before the text to make it long enough. If the text is longer
* than requested, no changes are made.
*
* @param text text to be padded
* @param length requested length
* @return padded text
*/
global define string PadZeros(string text, integer length) {
if(text == nil) text = "";
integer rest = length - size(text);
string pad = "";
while(rest > 0) {
pad = pad + "0";
rest = rest - 1;
}
return pad + text;
}
/**
* Parse string of values
*
* Parse string of values - split string to values, quoting and backslash sequences are supported
* @param options Input string
* @param parameters Parmeter used at parsing - map with keys:
*"separator":<string> - value separator (default: " \t"),
*"unique":<boolean> - result will not contain any duplicates, first occurance of the string is stored into output (default: false),
*"interpret_backslash":<boolean> - convert backslash sequence into one character (e.g. "\\n" => "\n") (default: true)
*"remove_whitespace":<boolean> - remove white spaces around values (default: true),
* @return list<string> List of strings
*/
global define list<string> ParseOptions(string options, map parameters) ``{
list<string> ret = [];
// parsing options
string separator = parameters["separator"]:" \t";
boolean unique = parameters["unique"]:false;
boolean interpret_backslash = parameters["interpret_backslash"]:true;
boolean remove_whitespace = parameters["remove_whitespace"]:true;
y2debug("Input: string: '%1', parameters: %2", options, parameters);
y2debug("Used values: separator: '%1', unique: %2, remove_whitespace: %3",
separator, unique, remove_whitespace);
if (options == nil)
{
return [];
}
// two algorithms are used:
// first is much faster, but only usable if string
// doesn't contain any double qoute characters
// and backslash sequences are not interpreted
// second is more general, but of course slower
if (findfirstof(options, "\"") == nil && interpret_backslash == false)
{
// easy case - no qouting, don't interpres backslash sequences => use splitstring
list<string> values = splitstring(options, separator);
foreach (string v, values, ``{
if (remove_whitespace == true)
{
v = CutBlanks(v);
}
if (unique == true)
{
if (!contains(ret, v)) ret = add(ret, v);
}
else
{
ret = add(ret, v);
}
});
}
else
{
// quoting is used or backslash interpretation is enabled
// so it' not possible to split input
// parsing each character is needed - use finite automaton
// state
symbol state = `out_of_string;
// position in the input string
integer index = 0;
// parsed value - buffer
string str = "";
while(index < size(options))
{
string character = substring(options, index, 1);
y2debug("character: %1 state: %2 index: %3", character, state, index);
// interpret backslash sequence
if (character == "\\" && interpret_backslash == true)
{
if (index + 1 < size(options))
{
string nextcharacter = substring(options, index + 1, 1);
index = index + 1;
// backslah sequences
map backslash_seq = $[
"a" : "\a", // alert
"b" : "\b", // backspace
"e" : "\e", // escape
"f" : "\f", // FF
"n" : "\n", // NL
"r" : "\r", // CR
"t" : "\t", // tab
"v" : "\v", // vertical tab
"\\": "\\", // backslash
];
if (haskey(backslash_seq, nextcharacter) == true)
{
character = backslash_seq[nextcharacter]:"DUMMY";
}
else
{
if (nextcharacter != "\"")
{
// ignore backslash in invalid backslash sequence
character = nextcharacter;
}
else
{
// backslash will be removed later,
// double quote and escaped double quote have to different yet
character = "\\\"";
}
}
y2debug("backslash sequence: '%1'", character);
}
else
{
y2warning("Missing character after backslash (\\) at the end of string");
}
}
if (state == `out_of_string)
{
// ignore separator or white space at the beginning of the string
if (issubstring(separator, character) == true || (remove_whitespace == true && (character == " " || character == "\t")) )
{
index = index + 1;
continue;
}
// start of a quoted string
else if (character == "\"")
{
state = `in_quoted_string;
}
else
{
// start of a string
state = `in_string;
if (character == "\\\"")
{
str = "\"";
}
else
{
str = character;
}
}
}
// after double quoted string - handle non-separator chars after double quote
else if (state == `in_quoted_string_after_dblqt)
{
if (issubstring(separator, character) == true)
{
if (unique == true)
{
if (!contains(ret, str)) ret = add(ret, str);
}
else
{
ret = add(ret, str);
}
str = "";
state = `out_of_string;
}
else if (character == "\\\"")
{
str = str + "\"";
}
else
{
str = str + character;
}
}
else if (state == `in_quoted_string)
{
if (character == "\"")
{
// end of quoted string
state = `in_quoted_string_after_dblqt;
}
else if (character == "\\\"")
{
str = str + "\"";
}
else
{
str = str + character;
}
}
else if (state == `in_string)
{
if (issubstring(separator, character) == true)
{
state = `out_of_string;
if (remove_whitespace == true)
{
str = CutBlanks(str);
}
if (unique == true)
{
if (!contains(ret, str)) ret = add(ret, str);
}
else
{
ret = add(ret, str);
}
str = "";
}
else if (character == "\\\"")
{
str = str + "\"";
}
else
{
str = str + character;
}
}
index = index + 1;
}
// error - still in quoted string
if (state == `in_quoted_string || state == `in_quoted_string_after_dblqt)
{
if (state == `in_quoted_string)
{
y2warning("Missing trainling double quote character(\") in input: '%1'", options);
}
if (unique == true)
{
if (!contains(ret, str)) ret = add(ret, str);
}
else
{
ret = add(ret, str);
}
}
// process last string in the buffer
if (state == `in_string)
{
if (remove_whitespace)
{
str = CutBlanks(str);
}
if (unique == true)
{
if (!contains(ret, str)) ret = add(ret, str);
}
else
{
ret = add(ret, str);
}
}
}
y2debug("Parsed values: %1", ret);
return ret;
}
/**
* Remove first or every match of given regular expression from a string
*
* (e.g. CutRegexMatch( "abcdef12ef34gh000", "[0-9]+", true ) -> "abcdefefgh",
* CutRegexMatch( "abcdef12ef34gh000", "[0-9]+", false ) -> "abcdefef34gh000")
*
* @param input string that might occur regex
* @param regex regular expression to search for, must not contain brackets
* @param glob flag if only first or every occuring match should be removed
* @return string that has matches removed
*/
global define string CutRegexMatch(string input, string regex, boolean glob) ``{
if(input == nil || size(input) < 1) return "";
string output = input;
if( regexpmatch( output, regex ) )
{
list p = regexppos( output, regex );
do
{
output = substring( output, 0, p[0]:0 ) +
substring( output, p[0]:0+p[1]:0 );
p = regexppos( output, regex );
}
while( glob && size(p)>0 );
}
return output;
}
/**
* Function for escaping (replacing) (HTML|XML...) tags with their
* (HTML|XML...) meaning.
*
* Usable to present text "as is" in RichText.
*
* @param string text to escape
* @return string escaped text
*/
global define string EscapeTags (string text) ``{
text = mergestring(splitstring(text, "&"), "&");
text = mergestring(splitstring(text, "<"), "<");
text = mergestring(splitstring(text, ">"), ">");
return text;
}
/**
* Shorthand for select (splitstring (s, separators), 0, "")
* Useful now that the above produces a deprecation warning.
* @param s string to be split
* @param separators characters which delimit components
* @return first component or ""
*/
global string FirstChunk (string s, string separators) {
list <string> l = splitstring (s, separators);
return l[0]:"";
}
// character sets, suitable for ValidChars
string cupper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
string clower = "abcdefghijklmnopqrstuvwxyz";
string calpha = cupper + clower;
string cdigit = "0123456789";
string cxdigit = cdigit + "ABCDEFabcdef";
string calnum = calpha + cdigit;
string cpunct = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
string cgraph = calnum + cpunct;
string cspace = " \f\r\n\t\013";
string cprint = cspace + cgraph;
/**
* @return the 26 uppercase ASCII letters
*/
global string CUpper () {
return cupper;
}
/**
* @return the 26 lowercase ASCII letters
*/
global string CLower () {
return clower;
}
/**
* @return the 52 upper and lowercase ASCII letters
*/
global string CAlpha () {
return calpha;
}
/**
* @return 0123456789
*/
global string CDigit () {
return cdigit;
}
/**
* @return hexadecimal digits: 0123456789ABCDEFabcdef
*/
global string CXdigit () {
return cxdigit;
}
/**
* @return the 52 upper and lowercase ASCII letters and digits
*/
global string CAlnum () {
return calnum;
}
/**
* @return the ASCII printable non-blank non-alphanumeric characters
*/
global string CPunct () {
return cpunct;
}
/**
* @return printable ASCII charcters except whitespace
*/
global string CGraph () {
return cgraph;
}
/**
* @return ASCII whitespace
*/
global string CSpace () {
return cspace;
}
/**
* @return printable ASCII characters including whitespace
*/
global string CPrint () {
return cprint;
}
/**
* Characters valid in a filename (not pathname).
* Naturally "/" is disallowed. Otherwise, the graphical ASCII
* characters are allowed.
* @return string for ValidChars
*/
global string ValidCharsFilename () {
return deletechars (CGraph (), "/");
}
// 64 characters is the base undeline length
string base_underline = "----------------------------------------------------------------";
/* - hidden for documentation -
*
* Local function for finding longest records in the table.
*
* @param list <list <string> > table items
* @return list <integer> longest records by columns
*/
list <integer> FindLongestRecords (list <list <string> > items) {
list <integer> longest = [];
// searching all rows
foreach (list <string> row, items, {
// starting with column 0
integer col_counter = 0;
// testing all columns on the row
foreach (string col, row, {
integer col_size = size(col);
// found longer record for this column
if (col_size>longest[col_counter]:-1) longest[col_counter] = col_size;
// next column
col_counter = col_counter + 1;
});
});
return longest;
}
/* - hidden for documentation -
*
* Local function creates table row.
*
* @param list <string> row items
* @param list <integer> columns lengths
* @param integer record horizontal padding
* @return string padded table row
*/
string CreateTableRow (list <string> row_items, list <integer> cols_lenghts, integer horizontal_padding) {
string row = "";
integer col_counter = 0;
integer records_count = size(row_items) - 1;
foreach (string record, row_items, {
integer padding = cols_lenghts[col_counter]:0;
if (col_counter<records_count) padding = padding + horizontal_padding;
row = row + String::Pad(record, padding);
col_counter = col_counter + 1;
});
return row;
}
/* - hidden for documentation -
*
* Local function returns underline string /length/ long.
*
* @param integer length of underline
* @return string /length/ long underline
*/
string CreateUnderline (integer length) {
string underline = base_underline;
while (size(underline)<length) {
underline = underline + base_underline;
}
underline = substring(underline, 0, length);
return underline;
}
/* - hidden for documentation -
*
* Local function for creating header underline for table.
* It uses maximal lengths of records defined in cols_lenghts.
*
* @param list <integer> maximal lengths of records in columns
* @param integer horizontal padding of records
* @return string table header underline
*/
string CreateTableHeaderUnderline (list <integer> cols_lenghts, integer horizontal_padding) {
integer col_counter = 0;
// count of added paddings
integer records_count = size(cols_lenghts) - 1;
// total length of underline
integer total_size = 0;
foreach (integer col_size, cols_lenghts, {
total_size = total_size + col_size;
// adding padding where necessary
if (col_counter<records_count) {
total_size = total_size + horizontal_padding;
}
col_counter = col_counter + 1;
});
return CreateUnderline(total_size);
}
/**
* Function creates text table without using HTML tags.
* (Useful for commandline)
* Undefined option uses the default one.
*
* @param list <string> header
* @param list <list <string> > items
* @param map <string, string> options
* @return string table
*
* Header: [ "Id", "Configuration", "Device" ]
* Items: [ [ "1", "aaa", "Samsung Calex" ], [ "2", "bbb", "Trivial Trinitron" ] ]
* Possible Options: horizontal_padding (for columns), table_left_padding (for table)
*/
global define string TextTable (list<string> header, list <list <string> > items, map <string, any> options) {
integer current_horizontal_padding = (integer) options["horizontal_padding"]:2;
integer current_table_left_padding = (integer) options["table_left_padding"]:4;
list <integer> cols_lenghts = FindLongestRecords(add(items, header));
// whole table is left-padded
string table_left_padding = String::Pad("", current_table_left_padding);
// the last row has no newline
integer rows_count = size(items);
string table = "";
table = table + table_left_padding
+ CreateTableRow(header, cols_lenghts, current_horizontal_padding) + "\n";
table = table + table_left_padding
+ CreateTableHeaderUnderline(cols_lenghts, current_horizontal_padding) + "\n";
integer rows_counter = 1;
foreach (list <string> row, items, {
table = table + table_left_padding
+ CreateTableRow(row, cols_lenghts, current_horizontal_padding) +
(rows_counter<rows_count ? "\n":"");
rows_counter = rows_counter + 1;
});
return table;
}
/**
* Function returns underlined text header without using HTML tags.
* (Useful for commandline)
*
* @param string header line
* @param integer left padding
* @return string underlined header line
*/
global define string UnderlinedHeader (string header_line, integer left_padding) {
return
Pad("", left_padding) + header_line + "\n" +
Pad("", left_padding) + CreateUnderline(size(header_line));
}
//////////////////////////////////////////
// sysconfig metadata related functions //
//////////////////////////////////////////
/**
* Get metadata lines from input string
* @param input Input string - complete comment of a sysconfig variable
* @return list<string> Metadata lines in list
*/
global define list<string> GetMetaDataLines(string input) ``{
if (input == nil || input == "")
{
return [];
}
list<string> lines = splitstring(input, "\n");
return (filter(string line, lines, ``(regexpmatch(line, "^##.*"))));
}
/**
* Get comment without metadata
* @param input Input string - complete comment of a sysconfig variable
* @return string Comment used as variable description
*/
global define string GetCommentLines(string input) ``{
if (input == nil || input == "")
{
return "";
}
list<string> lines = splitstring(input, "\n");
string ret = "";
foreach(string line, lines, ``{
string com_line = regexpsub(line, "^#([^#].*)", "\\1");
if (com_line == nil)
{
// add empty lines
if (regexpmatch(line, "^#[ \t]*$") == true)
{
ret = ret + "\n";
}
}
else
{
ret = ret + com_line + "\n";
}
}
);
return ret;
}
/**
* Parse metadata from a sysconfig comment
* @param comment comment of a sysconfig variable (single line or multiline string)
* @return map parsed metadata
*/
global define map<string, string> ParseSysconfigComment(string comment) ``{
map<string, string> ret = $[];
// get metadata part of comment
list<string> metalines = GetMetaDataLines(comment);
list<string> joined_multilines = [];
string multiline = "";
y2debug("metadata: %1", metalines);
// join multi line metadata lines
foreach(string metaline, metalines, ``{
if (substring(metaline, size(metaline) - 1, 1) != "\\")
{
if (multiline != "")
{
// this not first multiline so remove comment mark
string without_comment = regexpsub(metaline, "^##(.*)", "\\1");
if (without_comment != nil)
{
metaline = without_comment;
}
}
joined_multilines = add(joined_multilines, multiline + metaline);
multiline = "";
}
else
{
string part = substring(metaline, 0, size(metaline) - 1);
if (multiline != "")
{
// this not first multiline so remove comment mark
string without_comment = regexpsub(part, "^##(.*)", "\\1");
if (without_comment != nil)
{
part = without_comment;
}
}
// add line to the previous lines
multiline = multiline + part;
}
}
);
y2debug("metadata after multiline joining: %1", joined_multilines);
// parse each metadata line
foreach(string metaline, joined_multilines, ``{
/* Ignore lines with ### -- general comments */
if (regexpmatch(metaline, "^###"))
{
return;
}
string meta = regexpsub(metaline, "^##[ \t]*(.*)", "\\1");
// split sting to the tag and value part
integer colon_pos = findfirstof(meta, ":");
string tag = "";
string val = "";
if (colon_pos == nil)
{
// colon is missing
tag = meta;
}
else
{
tag = substring(meta, 0, colon_pos);
if (size(meta) > colon_pos + 1)
{
val = substring(meta, colon_pos + 1);
}
}
// remove whitespaces from parts
tag = CutBlanks(tag);
val = CutBlanks(val);
y2debug("tag: %1 val: '%2'", tag, val);
// add tag and value to map if they are present in comment
if (tag != "")
{
ret = (map<string, string>) add(ret, tag, val);
}
else
{
// ignore separator lines
if (!regexpmatch(metaline, "^#*$"))
{
y2warning("Unknown metadata line: %1", metaline);
}
}
}
);
y2debug("parsed sysconfig comment: %1", ret);
return ret;
}
/**
* Replace substring in a string. All substrings source are replaced by string target.
* @param s input string
* @param source the string which will be replaced
* @param target the new string which is used instead of source
* @return string result
*/
global string Replace(string s, string source, string target) {
if (s == nil)
{
return nil;
}
if (source == nil || source == "")
{
y2warning("invalid parameter source: %1", source);
return s;
}
if (target == nil)
{
y2warning("invalid parameter target: %1", target);
return s;
}
integer pos = find(s, source);
while (pos >= 0) {
string tmp = substring(s, 0, pos) + target;
if (size(s) > pos + size(source))
{
tmp = tmp + substring(s, pos + size(source));
}
s = tmp;
pos = find(s, source);
}
return s;
}
/**
* Returns text wrapped at defined margin. Very useful for translated strings
* used for pop-up windows or dialogs where you can't know the width. It
* controls the maximum width of the string so the text should allways fit into
* the minimal ncurses window. If you expect some long words, such us URLs or
* words with a hyphen inside, you can also set the additional split-characters
* to "/-". Then the function can wrap the word also after these characters.
* This function description was wrapped using the function String::WrapAt().
*
* @example String::WrapAt("Some very long text",30,"/-");
*
* @param string text to be wrapped
* @param integer maximum width of the wrapped text
* @param string additional split-characters such as "-" or "/"
* @return string wrapped string
*/
global string WrapAt(string text, integer width, string split_string) {
string new_string = "";
integer avail = width; // characters available in this line
string lsep = ""; // set to "\n" when at the beginning of a new line
string wsep = ""; // set to " " after words, unless at the beginning
foreach (string word, splitstring(text, " \n"), {
while (size(word) > 0) {
// decide where to split the current word
integer split_at = 0;
if (size(word) <= width) {
split_at = size(word);
} else {
split_at = findlastof(substring(word, 0, avail - size(wsep)),
" \n" + split_string);
if (split_at != nil) {
split_at = split_at + 1;
} else {
split_at = findlastof(substring(word, 0, width),
" \n" + split_string);
if (split_at != nil) {
split_at = split_at + 1;
} else {
split_at = avail - size(wsep);
}
}
}
// decide whether it fits into the same line or must go on
// a separate line
if (size(wsep) + split_at > avail) {
if (size(new_string) > 0)
new_string = new_string + "\n";
avail = width;
wsep = "";
lsep = "";
}
// add the next word or partial word
new_string = new_string + lsep + wsep +
substring(word, 0, split_at);
avail = avail - size(wsep) - split_at;
wsep = ""; lsep = "";
if (avail == 0) {
avail = width;
lsep = "\n";
} else if (split_at == size(word)) {
wsep = " ";
}
word = substring(word, split_at);
}
});
return new_string;
}
/**
* Make a random base-36 number.
* srandom should be called beforehand.
* @param len string length
* @return random string of 0-9 and a-z
*/
global string Random (integer len) {
if (len <= 0)
{
return "";
}
string digits = cdigit + clower; // uses the character classes from above
integer base = size (digits);
integer max = 1;
integer i = len;
while (i > 0)
{
max = max * base;
i = i - 1;
}
integer rnum = random (max);
string ret = "";
i = len;
while (i > 0)
{
integer digit = rnum % base;
rnum = rnum / base;
ret = ret + substring (digits, digit, 1);
i = i -1;
}
return ret;
}
/* EOF */
}