Printf

From Second Life Wiki
Revision as of 19:08, 29 May 2023 by Pazako Karu (talk | contribs) (→‎Code: fix() function was improved, works with any valid precision and is way faster.)
Jump to navigation Jump to search

To the best of my ability, this replicates the printf() function as it exists in other languages. Generally speaking, you write a string with some format specifiers, and then send it the variables, rather than having to concatenate a bunch of strings and conversions together.

%[align][width][.precision]flag

Align supports -, =, + for left, center, and right align.

Width will pad spaces if the result string doesn't fit. It doesn't crop one.

Precision limits decimal spaces of a float. 0 makes it an integer

Flag determines input type, with %s string, %d integer, %f float, %v vector, and %r rotation supported.

A vector or rotation will have pad/align/precision applied to each of its sub-parts, rather than the whole thing. This just made more sense to me. And yes technically, this is sprintf, but I did add a printf function to ownersay the results.

Code

string pad(string value, integer width, string alignment)
{
    integer len = llStringLength(value);
    if (width > len)
    {
        string spacers = "    ";
        while (llStringLength(spacers) < width-len) // This works fastest with a maxed width constant, but can be dynamic
            spacers += spacers; // Expand spacers while necessary
        spacers = llGetSubString(spacers, 0, width-len-1);
        
        if (alignment == "-") // left align
            return value+spacers;
        else if (alignment == "+") // right align
            return spacers+value;
        else if (alignment == "=") // center align
            return llInsertString(spacers, (width-len)/2, value);
    }
    return value;
}
string fix(float value, integer precision)
{//Set the decimal precision of a float. Technically only affects decimal places
    if (precision < 1) precision = -1;
    if (precision > 6) precision = 6;
    return llGetSubString((string)value, 0, precision-7);
}
printf(string input, list values)
{
    llOwnerSay(sprintf(input, values));
}
string sprintf(string input, list values)
{
    integer iValue; // Which value will replace
    integer iValues = llGetListLength(values); // Number of values to replace
    for (iValue = 0; iValue < iValues; iValue++)
    {
        integer index = llSubStringIndex(input, "%"); // "%=99.9v " tag found
        
        if (~index)
        {
            string buffer = llGetSubString(input, index, index+12); //  A few chars past % into a buffer
            integer bufferLen = llSubStringIndex(buffer, " "); // A space or newline ends buffer
            if (!~bufferLen)
                bufferLen = llSubStringIndex(buffer, "\n");
            buffer = llGetSubString(buffer, 0, bufferLen); // Trim buffer to new length
            
            integer alignIndex;
            string alignment = "+"; // Default to right align
            
            if (~llSubStringIndex(buffer, "=")) // Center align
                alignment = "=";
            else if (~llSubStringIndex(buffer, "-")) // Left align
                alignment = "-";
            else if (~llSubStringIndex(buffer, "+")) // Right align
                alignment = "+";
            else
                alignIndex--; // There is no symbol, move index back for (integer) typecast later //*/
            
            integer width = (integer)llGetSubString(buffer, 2+alignIndex, -1);
            integer precision = 0; 
            integer dot = llSubStringIndex(buffer, "."); // Location of period, if any
            if (~dot) // If there is a dot, the integer which follows is our precision
                precision = (integer)llGetSubString(buffer, 1+dot, -1);
            
            string formatted; // Result of formatting to replace into string
            
            if (~llSubStringIndex(buffer, "f")) // if type == float
            {
                float value = llList2Float(values, iValue);
                formatted = fix(value, precision);    
            }
            else if (~llSubStringIndex(buffer, "v")) // if type == vector
            {
                vector value = (vector)llList2String(values, iValue);
                formatted = (string)["<", pad(fix(value.x, precision), width, alignment), ",",
                                          pad(fix(value.y, precision), width, alignment), ",",
                                          pad(fix(value.z, precision), width, alignment), ">"];
            }
            else if (~llSubStringIndex(buffer, "r")) // if type == rotation
            {
                rotation value = (rotation)llList2String(values, iValue);
                formatted = (string)["<", pad(fix(value.x, precision), width, alignment), ",",
                                          pad(fix(value.y, precision), width, alignment), ",",
                                          pad(fix(value.z, precision), width, alignment), ",",
                                          pad(fix(value.s, precision), width, alignment), ">"];
            } // Because of typecasting in a list, "s" and "d" are identical, so catch all at the end
            else formatted = llList2String(values, iValue);
            
            // insert formatted value back into source string, and a space
            input = llInsertString(llDeleteSubString(input, index, index+bufferLen),index, pad(formatted, width, alignment)+" ");
            // cant use this, faster but we only want to replace one value, this does all.
            //input = llDumpList2String(llParseStringKeepNulls(input, [llGetSubString(input, index, index+bufferLen)], []), pad(formatted, width, alignment)+" ");
        }
    }
    return input;
}
default
{
    state_entry()
    {
        printf("This script has been running for %0.2f seconds.", [llGetTime()]);
    }
}

Example with testing functions included

integer TRIALS = 5000;
string pad(string value, integer width, string alignment)
{
    integer len = llStringLength(value);
    if (width > len)
    {
        string spacers = "    ";
        while (llStringLength(spacers) < width-len) // This works fastest with a maxed width constant, but can be dynamic
            spacers += spacers; // Expand spacers while necessary
        spacers = llGetSubString(spacers, 0, width-len-1);
        
        if (alignment == "-") // left align
            return value+spacers;
        else if (alignment == "+") // right align
            return spacers+value;
        else if (alignment == "=") // center align
            return llInsertString(spacers, (width-len)/2, value);
    }
    return value;
}
string fix(float value, integer precision)
{//Set the decimal precision of a float. Technically only affects decimal places
    integer pre = (integer)value;
    // WARNING !! Rounding is occurring here: Not rounding left 69.1 as 69.099998 => 69.0
    integer post = llRound(llPow(10,precision)*(value-pre)); // In linux, rounding is expected behavior
    if (precision > 0)
        return (string)pre + "." + llGetSubString((string)post+"0000000", 0, precision-1);
    return (string)pre;
}
printf(string input, list values)
{
    llOwnerSay(sprintf(input, values));
}
string sprintf(string input, list values)
{
    integer iValue; // Which value will replace
    integer iValues = llGetListLength(values); // Number of values to replace
    for (iValue = 0; iValue < iValues; iValue++)
    {
        integer index = llSubStringIndex(input, "%"); // "%=99.9v " tag found
        
        if (~index)
        {
            string buffer = llGetSubString(input, index, index+12); //  A few chars past % into a buffer
            integer bufferLen = llSubStringIndex(buffer, " "); // A space or newline ends buffer
            if (!~bufferLen)
                bufferLen = llSubStringIndex(buffer, "\n");
            buffer = llGetSubString(buffer, 0, bufferLen); // Trim buffer to new length
            
            integer alignIndex;
            string alignment = "+"; // Default to right align
            
            if (~llSubStringIndex(buffer, "=")) // Center align
                alignment = "=";
            else if (~llSubStringIndex(buffer, "-")) // Left align
                alignment = "-";
            else if (~llSubStringIndex(buffer, "+")) // Right align
                alignment = "+";
            else
                alignIndex--; // There is no symbol, move index back for (integer) typecast later //*/
            
            integer width = (integer)llGetSubString(buffer, 2+alignIndex, -1);
            integer precision = 0; 
            integer dot = llSubStringIndex(buffer, "."); // Location of period, if any
            if (~dot) // If there is a dot, the integer which follows is our precision
                precision = (integer)llGetSubString(buffer, 1+dot, -1);
            
            string formatted; // Result of formatting to replace into string
            
            if (~llSubStringIndex(buffer, "f")) // if type == float
            {
                float value = llList2Float(values, iValue);
                formatted = fix(value, precision);    
            }
            else if (~llSubStringIndex(buffer, "v")) // if type == vector
            {
                vector value = (vector)llList2String(values, iValue);
                formatted = (string)["<", pad(fix(value.x, precision), width, alignment), ",",
                                          pad(fix(value.y, precision), width, alignment), ",",
                                          pad(fix(value.z, precision), width, alignment), ">"];
            }
            else if (~llSubStringIndex(buffer, "r")) // if type == rotation
            {
                rotation value = (rotation)llList2String(values, iValue);
                formatted = (string)["<", pad(fix(value.x, precision), width, alignment), ",",
                                          pad(fix(value.y, precision), width, alignment), ",",
                                          pad(fix(value.z, precision), width, alignment), ",",
                                          pad(fix(value.s, precision), width, alignment), ">"];
            } // Because of typecasting in a list, "s" and "d" are identical, so catch all at the end
            else formatted = llList2String(values, iValue);
            
            // insert formatted value back into source string, and a space
            input = llInsertString(llDeleteSubString(input, index, index+bufferLen),index, pad(formatted, width, alignment)+" ");
            // cant use this, faster but we only want to replace one value, this does all.
            //input = llDumpList2String(llParseStringKeepNulls(input, [llGetSubString(input, index, index+bufferLen)], []), pad(formatted, width, alignment)+" ");
        }
    }
    return input;
}
testPad() // string pad(string value, integer width, string alignment)
{ // 5000 trials completed in 7.71 seconds, 1.5 ms each.
    string TEST_STRING = "ABC";
    integer WIDTH_LIMIT = 60; // Max whitespace for padding test
    integer i; 
    integer j;
    float t1 = llGetTime();
    for (i = 0; i < TRIALS; i+= 3) // One for each align, so skip 3
    {
        j = i % WIDTH_LIMIT;
        pad(TEST_STRING, j+0, "-"); // Left align
        pad(TEST_STRING, j+1, "="); // Center align
        pad(TEST_STRING, j+2, "+"); // Right align
    }
    float t2 = llGetTime();
    llOwnerSay(sprintf("%d trials completed in %0.2f seconds, %0.3f ms each.", [TRIALS, t2-t1, 1E3*(t2-t1)/TRIALS]));
}
testFix() // string fix(float value, integer precision)
{ // 5000 trials completed in 23.67 seconds, 4.7 ms each.
    integer i; 
    integer j;
    float t1 = llGetTime();
    for (i = 0; i < TRIALS; i++)
        for (j = 0; j <= 6; j++) // Precision is meaningless past 6 in lsl
            fix(i, j);
    float t2 = llGetTime();
    llOwnerSay(sprintf("%d trials completed in %0.2f seconds, %0.3f ms each.", [TRIALS, t2-t1, 1E3*(t2-t1)/TRIALS]));
}
testSprintf()
{
    float t1 =  llGetTime();
    list tests = [  sprintf("%12.5r", [ZERO_ROTATION]),
                    sprintf("%12.0r", [ZERO_ROTATION]),
                    sprintf("%0.5r", [ZERO_ROTATION]),
                    sprintf("%12.r", [ZERO_ROTATION]),
                    sprintf("%.5r", [ZERO_ROTATION]),
                    sprintf("%.r", [ZERO_ROTATION]),
                    sprintf("%r", [ZERO_ROTATION]),
                    sprintf("%-12r", [ZERO_ROTATION]),
                    sprintf("%=12r", [ZERO_ROTATION]),
                    sprintf("%+12r", [ZERO_ROTATION]),
                    sprintf("%12.5v", [ZERO_VECTOR]),
                    sprintf("%12.0v", [ZERO_VECTOR]),
                    sprintf("%0.5v", [ZERO_VECTOR]),
                    sprintf("%12.v", [ZERO_VECTOR]),
                    sprintf("%.5v", [ZERO_VECTOR]),
                    sprintf("%.v", [ZERO_VECTOR]),
                    sprintf("%v", [ZERO_VECTOR]),
                    sprintf("%-12v", [ZERO_VECTOR]),
                    sprintf("%=12v", [ZERO_VECTOR]),
                    sprintf("%+12v", [ZERO_VECTOR]),
                    sprintf("%12.5f", [0]),
                    sprintf("%12.0f", [0]),
                    sprintf("%0.5f", [0]),
                    sprintf("%12.f", [0]),
                    sprintf("%.5f", [0]),
                    sprintf("%.f", [0]),
                    sprintf("%f", [0]),
                    sprintf("%-12f", [0]),
                    sprintf("%=12f", [0]),
                    sprintf("%+12f", [0]),
                    sprintf("%12.5d", [0]),
                    sprintf("%12.0d", [0]),
                    sprintf("%0.5d", [0]),
                    sprintf("%12.d", [0]),
                    sprintf("%.5d", [0]),
                    sprintf("%.d", [0]),
                    sprintf("%d", [0]),
                    sprintf("%-12d", [0]),
                    sprintf("%=12d", [0]),
                    sprintf("%+12d", [0]),
                    sprintf("%12.5s", ["Test"]),
                    sprintf("%12.0s", ["Test"]),
                    sprintf("%0.5s", ["Test"]),
                    sprintf("%12.s", ["Test"]),
                    sprintf("%.5s", ["Test"]),
                    sprintf("%.s", ["Test"]),
                    sprintf("%12s", ["Test"]),
                    sprintf("%-12s", ["Test"]),
                    sprintf("%=12s", ["Test"]),
                    sprintf("%+12s", ["Test"])];
                    
    integer i;
    integer len = llGetListLength(tests);
    string out;
    for (i = 0; i < len; i++)
    {
        if (!(llList2String(tests, i) == llList2String(results, i)))
            out += "\n"+sprintf("Test #%d, %s != %s", [i, llList2String(tests, i), llList2String(results, i)]);
    }
    float t2 =  llGetTime();
    if (out)
         llOwnerSay("Failures: " + out);
    else llOwnerSay(sprintf("%d tests passed in %.2f seconds with an average run time of %.2f ms",[len, t2-t1, 1000*(t2-t1)/len]));
}
// Answers to the test
list results = [// rots
                    "<     0.00000,     0.00000,     0.00000,     1.00000> ",
                    "<           0,           0,           0,           1> ",
                    "<0.00000,0.00000,0.00000,1.00000> ",
                    "<           0,           0,           0,           1> ",
                    "<0.00000,0.00000,0.00000,1.00000> ",
                    "<0,0,0,1> ",
                    "<0,0,0,1> ",
                    "<0           ,0           ,0           ,1           > ",
                    "<     0      ,     0      ,     0      ,     1      > ",
                    "<           0,           0,           0,           1> ",
                    // vectors
                    "<     0.00000,     0.00000,     0.00000> ",
                    "<           0,           0,           0> ",
                    "<0.00000,0.00000,0.00000> ",
                    "<           0,           0,           0> ",
                    "<0.00000,0.00000,0.00000> ",
                    "<0,0,0> ",
                    "<0,0,0> ",
                    "<0           ,0           ,0           > ",
                    "<     0      ,     0      ,     0      > ",
                    "<           0,           0,           0> ",
                    // Floats
                    "     0.00000 ",
                    "           0 ",
                    "0.00000 ",
                    "           0 ",
                    "0.00000 ",
                    "0 ",
                    "0 ",
                    "0            ",
                    "     0       ",
                    "           0 ",
                    // Integers
                    "           0 ",
                    "           0 ",
                    "0 ",
                    "           0 ",
                    "0 ",
                    "0 ",
                    "0 ",
                    "0            ",
                    "     0       ",
                    "           0 ",
                    "        Test ",
                    "        Test ",
                    "Test ",
                    "        Test ",
                    "Test ",
                    "Test ",
                    "        Test ",
                    "Test         ",
                    "    Test     ",
                    "        Test "];

default
{
    state_entry()
    {
        testPad();
        testFix();
        testSprintf();
        printf("This script has been running for %0.2f seconds.", [llGetTime()]);
    }
}

Notes / Copyright

You are free to replicate or otherwise use this code to your heart's content. If you have suggestions for optimization, I'm all ears.