/*
 * comma - convert numbers into strings with 3-digit group and integer-fraction separators
 *
 * Convert numbers into strings with 3-digit group and integer-fraction separators.
 *
 * If the value is an integer, the integer-fraction separator is not used.
 *
 * str_comma(x, [group, [decimal]])
 *
 *	Convert x into a string.
 *
 *	If group is given and is a string, group will be used as
 *	the 3-digit group separator, otherwise the default 3-digit
 *	group separator will be used.
 *
 *	If decimal is given and is a string, group will be used as
 *	the integer-fraction separator, otherwise the default
 *	integer-fraction separator will be used.
 *
 *	The decimal and group arguments are optional.
 *
 * set_default_group_separator(group)
 *
 *	Change the default 3-digit group separator if group is a string,
 *	otherwise the default 3-digit group separator will not be
 *	changed.  Return the old 3-digit group separator.
 *
 * set_default_decimal_separator(decimal)
 *
 *	Change the default 3-digit group separator if decimal is a
 *	string, otherwise the default integer-fraction separator
 *	will not be changed.  Return the old integer-fraction separator.
 *
 * print_comma(x, [group, [decimal]])
 *
 *	Print the value produced by str_comma(x, [group, [decimal]])
 *	followed by a newline.
 *
 *	If the str_comma() does not return a string, nothing is printed.
 *
 *	The decimal and group arguments are optional.
 *
 *	The value produced by str_comma() is returned.
 *
 * fprint_comma(fd, x, [group, [decimal]])
 *
 *	Print the value produced by str_comma(x, [group, [decimal]]),
 *	without a trailing newline, on file fd.
 *
 *	If the str_comma() does not return a string, nothing is printed.
 *
 *	If fd is not an open file, nothing is printed.
 *
 *	The decimal and group arguments are optional.
 *
 *	The value produced by str_comma() is returned.
 *
 * Copyright (C) 2022  Landon Curt Noll
 *
 * Calc is open software; you can redistribute it and/or modify it under
 * the terms of the version 2.1 of the GNU Lesser General Public License
 * as published by the Free Software Foundation.
 *
 * Calc 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.
 *
 * A copy of version 2.1 of the GNU Lesser General Public License is
 * distributed with calc under the filename COPYING-LGPL.  You should have
 * received a copy with calc; if not, write to Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Under source code control:   2022/06/20 15:51:49
 * File existed as early as:    2022
 *
 * Share and enjoy!  :-)        http://www.isthe.com/chongo/tech/comp/calc/
 */


static default_group_separator = ",";	/* default 3-digit group separator */
static default_decimal_separator = ".";	/* default integer-fraction separator */


/*
 * str_comma - convert number into base 10 string with 3-digit groups and integer-fraction separator
 *
 *
 * This function converts a real number into a base 10 string, where
 * groups of 3 digits are separated by a 3-digit group separator and
 * a integer-fraction separator is printed between integer and decimal fraction.
 *
 * For example:
 *
 *	string = str_comma(x);
 *	string = str_comma(x), " ", ".");
 *	string = str_comma(x), ".", ",");
 *
 * Internally the function calls:
 *
 *     strprintf("%f", x);
 *
 * and thus the number of decimal fraction digits is subject to
 * the display() or config("display") value.  See:
 *
 *     man display
 *
 * for details.
 *
 * given:
 *
 *	x		number to convert
 *
 * optional args:
 *
 *	group		use this 3-digit group separator
 *	decimal		use this integer-fraction separator
 *
 * returns:
 *
 *	string containing the base 10 digits with group and decimal separators, OR
 *	null() if x is not a number, OR
 *	null() if group is neither null() (not given) nor a string, OR
 *	null() if group is null() (not given) AND default_group_separator is not a string, OR
 *	null() if decimal is neither null() (not given) nor a string, OR
 *	null() if decimal is null() (not given) AND default_decimal_separator is not a string.
 */
define str_comma(x, group, decimal)
{
    local group_separator;	/* 3-digit group separator */
    local decimal_separator;	/* separator between decimal integer and decimal fraction */
    local sign_str;		/* leading - if x < 0 or empty if x >= 0 */
    local integer;		/* integer part of absolute value of x */
    local int_str;		/* integer as a string */
    local int_len;		/* number of digits in int_str */
    local first_group_len;	/* length of 1st group before the 1st 3-digit group separator */
    local fraction;		/* factional part of absolute value of x */
    local frac_str;		/* fraction as a string */
    local frac_len;		/* number of digits in frac_str including leading 0. */
    local ret;			/* string to return */
    local config_leadzero;	/* config("leadzero") to restore */
    local config_tilde;		/* config("tilde") to restore */
    local i;

    /*
     * parse args - return null if args are bogus
     *
     * Return null() if args or conditions are bogus.
     */
    if (!isreal(x)) {
	return null();
    }
    group_separator = isnull(group) ? default_group_separator : group;
    decimal_separator = isnull(decimal) ? default_decimal_separator : decimal;
    if (!isstr(group_separator)) {
	return null();
    }
    if (!isstr(decimal_separator)) {
	return null();
    }

    /*
     * split number
     */
    if (x < 0) {
	sign_str = "-";
	integer = int(-x);
	fraction = frac(-x);
    } else {
	sign_str = "";
	integer = int(x);
	fraction = frac(x);
    }
    ret = sign_str;

    /*
     * convert digits
     *
     * Avoid printing ~ and leading zeros for factional part.
     */
    int_str = strprintf("%d", integer);
    config_leadzero = config("leadzero", 0);
    config_tilde = config("tilde", 0);
    frac_str = strprintf("%d", fraction);
    config("leadzero", config_leadzero),;
    config("tilde", config_tilde),;

    /*
     * determine number of digits in the integer part
     */
    int_len = strlen(int_str);
    frac_len = strlen(frac_str);

    /*
     * form integer part with group separators as needed
     */

    /*
     * case: integer is 3 or fewer digits
     */
    if (integer < 1000) {
	ret += int_str;

    /*
     * case: integer is 4 or more digits
     */
    } else {

	/*
	 * form a decimal string using group separators
	 */

	/*
	 * form the initial leading digits before 1st group separator
	 */
	first_group_len = int_len % 3;
	if (first_group_len == 0) {
	    first_group_len = 3;
	}
	ret += substr(int_str, 1, first_group_len);

	/*
	 * until end of digits, print group separator followed by 3 more digits
	 */
	for (i = first_group_len+1; i < int_len; i += 3) {
	    ret += group_separator + substr(int_str, i, 3);
	}
    }

    /*
     * form fractional part using decimal separator as needed
     */

    /*
     * case: x is an integer
     */
    if (fraction == 0) {

	/* no fraction, nothing more to do */

    /*
     * case: x is not an integer
     */
    } else {

	/*
	 * add integer-fraction separator
	 */
	ret += decimal_separator;

	/*
	 * add remaining digits
	 *
	 * Skip over the leading 0. in frac_str
	 */
	ret += substr(frac_str, 2, frac_len-1);
    }

    /*
     * All Done!!! -- Jessica Noll, Age 2
     */
    return ret;
}


/*
 * set_default_group_separator - change the default 3-digit group separator
 *
 * If group is not a string, then the default 3-digit group separator
 * is not changed.  Thus, this will only return the default 3-digit group separator:
 *
 *	set_default_group_separator(null());
 *
 * given:
 *
 *	group		3-digit group separator
 *
 * returns:
 *
 *	previous 3-digit group separator value
 */
define set_default_group_separator(group)
{
    local old_default_group_separator;		/* previous default 3-digit group separator to return */

    /*
     * save current 3-digit group separator
     */
    old_default_group_separator = default_group_separator;

    /*
     * change 3-digit group separator if group is a string
     */
    if (isstr(group)) {
	default_group_separator = group;
    }
    return old_default_group_separator;
}


/*
 * set_default_decimal_separator - change the default integer-fraction separator
 *
 * If decimal is not a string, then the default integer-fraction separator
 * is not changed.  Thus, this will only return the integer-fraction separator:
 *
 *	set_default_decimal_separator(null());
 *
 * given:
 *
 *	decimal		separator between decimal integer and decimal fraction (def: ".")
 *
 * returns:
 *
 *	previous integer-fraction separator value
 */
define set_default_decimal_separator(decimal)
{
    local old_default_decimal_separator;	/* previous default integer-fraction separator */

    /*
     * save current integer-fraction separator
     */
    old_default_decimal_separator = default_decimal_separator;

    /*
     * change 3-digit decimal integer-fraction if decimal is a string
     */
    if (isstr(decimal)) {
	default_decimal_separator = decimal;
    }
    return old_default_decimal_separator;
}


/*
 * print_comma - print base 10 string with 3-digit group separators & integer-fraction separator + newline
 *
 * This function prints the result of str_comma(x, group, decimal) followed by a newline.
 * For example:
 *
 *	print_comma(x);
 *	print_comma(x), " ", ".");
 *	print_comma(x), ".", ",");
 *
 * If str_comma() does not return a string, this function prints nothing.
 *
 * NOTE: To print without a newline, use fprint_comma(fd, x, group, decimal).
 *
 * given:
 *	x		number to convert
 *
 * optional args:
 *
 *	group		use this 3-digit group separator
 *	decimal		use this integer-fraction separator
 *
 * returns:
 *
 *	string containing the base 10 digits with group and decimal separators, OR
 *	null() if x is not a number, OR
 *	null() if group is neither null() (not given) nor a string, OR
 *	null() if group is null() (not given) AND default_group_separator is not a string, OR
 *	null() if decimal is neither null() (not given) nor a string, OR
 *	null() if decimal is null() (not given) AND default_decimal_separator is not a string.
 */
define print_comma(x, group, decimal)
{
    local ret;		/* base 10 string with 3-digit group and integer-fraction separators */

    /*
     * convert to string
     */
    ret = str_comma(x, group, decimal);

    /*
     * print converted string
     */
    if (isstr(ret)) {
	printf("%s\n", ret);
    }
    return ret;
}


/*
 * fprint_comma - print base 10 string with 3-digit group separators & integer-fraction separator w/o newline
 *
 * This function prints the result of str_comma(x, group, decimal) on an open file, without a trailing newline.
 * For example:
 *
 *	fprint_comma(files(1), x);
 *	fprint_comma(fd, x), " ", ".");
 *	fprint_comma(files(2), x), ".", ",");
 *
 * If str_comma() does not return a string, this function prints nothing.
 *
 * This function flushes output to the open file before returning.
 *
 * NOTE: To print with a newline, use print_comma(x, group, decimal).
 *
 * given:
 *	fd		open file
 *	x		number to convert
 *
 * optional args:
 *
 *	group		use this 3-digit group separator
 *	decimal		use this integer-fraction separator
 *
 * returns:
 *
 *	string containing the base 10 digits with group and integer-fraction separators, OR
 *	null() if x is not a number, OR
 *	null() if group is neither null() (not given) nor a string, OR
 *	null() if group is null() (not given) AND default_group_separator is not a string, OR
 *	null() if decimal is neither null() (not given) nor a string, OR
 *	null() if decimal is null() (not given) AND default_decimal_separator is not a string.
 */
define fprint_comma(fd, x, group, decimal)
{
    local ret;		/* base 10 string with 3-digit group and integer-fraction separators */

    /*
     * convert to string
     */
    ret = str_comma(x, group, decimal);

    /*
     * print converted string
     */
    if (isstr(ret) && isfile(fd)) {
	fprintf(fd, "%s", ret);
	fflush(fd);
    }
    return ret;
}
