* this version handles 64 bit numbers and so can hold values up to
* $92,233,720,368,547,758.07.
*
- * $PostgreSQL: pgsql/src/backend/utils/adt/cash.c,v 1.83 2010/07/16 02:15:53 tgl Exp $
+ * src/backend/utils/adt/cash.c
*/
#include "postgres.h"
#include <limits.h>
#include <ctype.h>
#include <math.h>
-#include <locale.h>
#include "libpq/pqformat.h"
#include "utils/builtins.h"
#include "utils/cash.h"
+#include "utils/int8.h"
#include "utils/numeric.h"
#include "utils/pg_locale.h"
-#define CASH_BUFSZ 36
-
-#define TERMINATOR (CASH_BUFSZ - 1)
-#define LAST_PAREN (TERMINATOR - 1)
-#define LAST_DIGIT (LAST_PAREN - 1)
-
/*************************************************************************
* Private routines
return buf;
} /* num_word() */
-
/* cash_in()
* Convert a string to a cash data type.
* Format is [$]###[,]###[.##]
Cash value = 0;
Cash dec = 0;
Cash sgn = 1;
- int seen_dot = 0;
+ bool seen_dot = false;
const char *s = str;
int fpoint;
- char dsymbol,
- ssymbol,
- psymbol;
- const char *nsymbol,
+ char dsymbol;
+ const char *ssymbol,
+ *psymbol,
+ *nsymbol,
*csymbol;
struct lconv *lconvert = PGLC_localeconv();
if (fpoint < 0 || fpoint > 10)
fpoint = 2; /* best guess in this case, I think */
- dsymbol = ((*lconvert->mon_decimal_point != '\0') ? *lconvert->mon_decimal_point : '.');
- if (*lconvert->mon_thousands_sep != '\0')
- ssymbol = *lconvert->mon_thousands_sep;
+ /* we restrict dsymbol to be a single byte, but not the other symbols */
+ if (*lconvert->mon_decimal_point != '\0' &&
+ lconvert->mon_decimal_point[1] == '\0')
+ dsymbol = *lconvert->mon_decimal_point;
else
- /* ssymbol should not equal dsymbol */
- ssymbol = (dsymbol != ',') ? ',' : '.';
- csymbol = ((*lconvert->currency_symbol != '\0') ? lconvert->currency_symbol : "$");
- psymbol = ((*lconvert->positive_sign != '\0') ? *lconvert->positive_sign : '+');
- nsymbol = ((*lconvert->negative_sign != '\0') ? lconvert->negative_sign : "-");
+ dsymbol = '.';
+ if (*lconvert->mon_thousands_sep != '\0')
+ ssymbol = lconvert->mon_thousands_sep;
+ else /* ssymbol should not equal dsymbol */
+ ssymbol = (dsymbol != ',') ? "," : ".";
+ csymbol = (*lconvert->currency_symbol != '\0') ? lconvert->currency_symbol : "$";
+ psymbol = (*lconvert->positive_sign != '\0') ? lconvert->positive_sign : "+";
+ nsymbol = (*lconvert->negative_sign != '\0') ? lconvert->negative_sign : "-";
#ifdef CASHDEBUG
- printf("cashin- precision '%d'; decimal '%c'; thousands '%c'; currency '%s'; positive '%c'; negative '%s'\n",
+ printf("cashin- precision '%d'; decimal '%c'; thousands '%s'; currency '%s'; positive '%s'; negative '%s'\n",
fpoint, dsymbol, ssymbol, csymbol, psymbol, nsymbol);
#endif
s++;
if (strncmp(s, csymbol, strlen(csymbol)) == 0)
s += strlen(csymbol);
+ while (isspace((unsigned char) *s))
+ s++;
#ifdef CASHDEBUG
printf("cashin- string is '%s'\n", s);
{
sgn = -1;
s += strlen(nsymbol);
-#ifdef CASHDEBUG
- printf("cashin- negative symbol; string is '%s'\n", s);
-#endif
}
else if (*s == '(')
{
sgn = -1;
s++;
}
- else if (*s == psymbol)
- s++;
+ else if (strncmp(s, psymbol, strlen(psymbol)) == 0)
+ s += strlen(psymbol);
#ifdef CASHDEBUG
printf("cashin- string is '%s'\n", s);
#endif
+ /* allow whitespace and currency symbol after the sign, too */
while (isspace((unsigned char) *s))
s++;
if (strncmp(s, csymbol, strlen(csymbol)) == 0)
s += strlen(csymbol);
+ while (isspace((unsigned char) *s))
+ s++;
#ifdef CASHDEBUG
printf("cashin- string is '%s'\n", s);
#endif
- for (;; s++)
+ /*
+ * We accumulate the absolute amount in "value" and then apply the sign at
+ * the end. (The sign can appear before or after the digits, so it would
+ * be more complicated to do otherwise.) Because of the larger range of
+ * negative signed integers, we build "value" in the negative and then
+ * flip the sign at the end, catching most-negative-number overflow if
+ * necessary.
+ */
+
+ for (; *s; s++)
{
/* we look for digits as long as we have found less */
/* than the required number of decimal places */
if (isdigit((unsigned char) *s) && (!seen_dot || dec < fpoint))
{
- value = (value * 10) + (*s - '0');
+ Cash newvalue = (value * 10) - (*s - '0');
+
+ if (newvalue / 10 != value)
+ ereport(ERROR,
+ (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+ errmsg("value \"%s\" is out of range for type %s",
+ str, "money")));
+
+ value = newvalue;
if (seen_dot)
dec++;
/* decimal point? then start counting fractions... */
else if (*s == dsymbol && !seen_dot)
{
- seen_dot = 1;
+ seen_dot = true;
}
/* ignore if "thousands" separator, else we're done */
- else if (*s != ssymbol)
- {
- /* round off */
- if (isdigit((unsigned char) *s) && *s >= '5')
- value++;
+ else if (strncmp(s, ssymbol, strlen(ssymbol)) == 0)
+ s += strlen(ssymbol) - 1;
+ else
+ break;
+ }
- /* adjust for less than required decimal places */
- for (; dec < fpoint; dec++)
- value *= 10;
+ /* round off if there's another digit */
+ if (isdigit((unsigned char) *s) && *s >= '5')
+ value--; /* remember we build the value in the negative */
- break;
- }
+ if (value > 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+ errmsg("value \"%s\" is out of range for type %s",
+ str, "money")));
+
+ /* adjust for less than required decimal places */
+ for (; dec < fpoint; dec++)
+ {
+ Cash newvalue = value * 10;
+
+ if (newvalue / 10 != value)
+ ereport(ERROR,
+ (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+ errmsg("value \"%s\" is out of range for type %s",
+ str, "money")));
+
+ value = newvalue;
}
- /* should only be trailing digits followed by whitespace or right paren */
+ /*
+ * should only be trailing digits followed by whitespace, right paren,
+ * trailing sign, and/or trailing currency symbol
+ */
while (isdigit((unsigned char) *s))
s++;
- while (isspace((unsigned char) *s) || *s == ')')
- s++;
- if (*s != '\0')
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
- errmsg("invalid input syntax for type money: \"%s\"", str)));
+ while (*s)
+ {
+ if (isspace((unsigned char) *s) || *s == ')')
+ s++;
+ else if (strncmp(s, nsymbol, strlen(nsymbol)) == 0)
+ {
+ sgn = -1;
+ s += strlen(nsymbol);
+ }
+ else if (strncmp(s, psymbol, strlen(psymbol)) == 0)
+ s += strlen(psymbol);
+ else if (strncmp(s, csymbol, strlen(csymbol)) == 0)
+ s += strlen(csymbol);
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("invalid input syntax for type %s: \"%s\"",
+ "money", str)));
+ }
- result = value * sgn;
+ /* If the value is supposed to be positive, flip the sign, but check for
+ * the most negative number. */
+ if (sgn > 0)
+ {
+ result = -value;
+ if (result < 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+ errmsg("value \"%s\" is out of range for type %s",
+ str, "money")));
+ }
+ else
+ result = value;
#ifdef CASHDEBUG
printf("cashin- result is " INT64_FORMAT "\n", result);
/* cash_out()
- * Function to convert cash to a dollars and cents representation.
- * XXX HACK This code appears to assume US conventions for
- * positive-valued amounts. - tgl 97/04/14
+ * Function to convert cash to a dollars and cents representation, using
+ * the lc_monetary locale's formatting.
*/
Datum
cash_out(PG_FUNCTION_ARGS)
{
Cash value = PG_GETARG_CASH(0);
char *result;
- char buf[CASH_BUFSZ];
- int minus = 0;
- int count = LAST_DIGIT;
- int point_pos;
- int ssymbol_position = 0;
+ char buf[128];
+ char *bufptr;
+ int digit_pos;
int points,
mon_group;
- char ssymbol;
- const char *csymbol,
- *nsymbol;
char dsymbol;
- char convention;
+ const char *ssymbol,
+ *csymbol,
+ *signsymbol;
+ char sign_posn,
+ cs_precedes,
+ sep_by_space;
struct lconv *lconvert = PGLC_localeconv();
/* see comments about frac_digits in cash_in() */
if (mon_group <= 0 || mon_group > 6)
mon_group = 3;
- convention = lconvert->n_sign_posn;
- dsymbol = ((*lconvert->mon_decimal_point != '\0') ? *lconvert->mon_decimal_point : '.');
- if (*lconvert->mon_thousands_sep != '\0')
- ssymbol = *lconvert->mon_thousands_sep;
+ /* we restrict dsymbol to be a single byte, but not the other symbols */
+ if (*lconvert->mon_decimal_point != '\0' &&
+ lconvert->mon_decimal_point[1] == '\0')
+ dsymbol = *lconvert->mon_decimal_point;
else
- /* ssymbol should not equal dsymbol */
- ssymbol = (dsymbol != ',') ? ',' : '.';
- csymbol = ((*lconvert->currency_symbol != '\0') ? lconvert->currency_symbol : "$");
- nsymbol = ((*lconvert->negative_sign != '\0') ? lconvert->negative_sign : "-");
-
- point_pos = LAST_DIGIT - points;
-
- point_pos -= (points - 1) / mon_group;
- ssymbol_position = point_pos % (mon_group + 1);
+ dsymbol = '.';
+ if (*lconvert->mon_thousands_sep != '\0')
+ ssymbol = lconvert->mon_thousands_sep;
+ else /* ssymbol should not equal dsymbol */
+ ssymbol = (dsymbol != ',') ? "," : ".";
+ csymbol = (*lconvert->currency_symbol != '\0') ? lconvert->currency_symbol : "$";
- /* we work with positive amounts and add the minus sign at the end */
if (value < 0)
{
- minus = 1;
+ /* make the amount positive for digit-reconstruction loop */
value = -value;
+ /* set up formatting data */
+ signsymbol = (*lconvert->negative_sign != '\0') ? lconvert->negative_sign : "-";
+ sign_posn = lconvert->n_sign_posn;
+ cs_precedes = lconvert->n_cs_precedes;
+ sep_by_space = lconvert->n_sep_by_space;
}
-
- /* allow for trailing negative strings */
- MemSet(buf, ' ', CASH_BUFSZ);
- buf[TERMINATOR] = buf[LAST_PAREN] = '\0';
-
- while (value || count > (point_pos - 2))
+ else
{
- if (points && count == point_pos)
- buf[count--] = dsymbol;
- else if (ssymbol && count % (mon_group + 1) == ssymbol_position)
- buf[count--] = ssymbol;
-
- buf[count--] = ((uint64) value % 10) + '0';
- value = ((uint64) value) / 10;
+ signsymbol = lconvert->positive_sign;
+ sign_posn = lconvert->p_sign_posn;
+ cs_precedes = lconvert->p_cs_precedes;
+ sep_by_space = lconvert->p_sep_by_space;
}
- strncpy((buf + count - strlen(csymbol) + 1), csymbol, strlen(csymbol));
- count -= strlen(csymbol) - 1;
+ /* we build the digits+decimal-point+sep string right-to-left in buf[] */
+ bufptr = buf + sizeof(buf) - 1;
+ *bufptr = '\0';
/*
- * If points == 0 and the number of digits % mon_group == 0, the code
- * above adds a trailing ssymbol on the far right, so remove it.
+ * Generate digits till there are no non-zero digits left and we emitted
+ * at least one to the left of the decimal point. digit_pos is the
+ * current digit position, with zero as the digit just left of the decimal
+ * point, increasing to the right.
*/
- if (buf[LAST_DIGIT] == ssymbol)
- buf[LAST_DIGIT] = '\0';
-
- /* see if we need to signify negative amount */
- if (minus)
+ digit_pos = points;
+ do
{
- result = palloc(CASH_BUFSZ + 2 - count + strlen(nsymbol));
+ if (points && digit_pos == 0)
+ {
+ /* insert decimal point, but not if value cannot be fractional */
+ *(--bufptr) = dsymbol;
+ }
+ else if (digit_pos < 0 && (digit_pos % mon_group) == 0)
+ {
+ /* insert thousands sep, but only to left of radix point */
+ bufptr -= strlen(ssymbol);
+ memcpy(bufptr, ssymbol, strlen(ssymbol));
+ }
- /* Position code of 0 means use parens */
- if (convention == 0)
- sprintf(result, "(%s)", buf + count);
- else if (convention == 2)
- sprintf(result, "%s%s", buf + count, nsymbol);
- else
- sprintf(result, "%s%s", nsymbol, buf + count);
- }
- else
+ *(--bufptr) = ((uint64) value % 10) + '0';
+ value = ((uint64) value) / 10;
+ digit_pos--;
+ } while (value || digit_pos >= 0);
+
+ /*----------
+ * Now, attach currency symbol and sign symbol in the correct order.
+ *
+ * The POSIX spec defines these values controlling this code:
+ *
+ * p/n_sign_posn:
+ * 0 Parentheses enclose the quantity and the currency_symbol.
+ * 1 The sign string precedes the quantity and the currency_symbol.
+ * 2 The sign string succeeds the quantity and the currency_symbol.
+ * 3 The sign string precedes the currency_symbol.
+ * 4 The sign string succeeds the currency_symbol.
+ *
+ * p/n_cs_precedes: 0 means currency symbol after value, else before it.
+ *
+ * p/n_sep_by_space:
+ * 0 No <space> separates the currency symbol and value.
+ * 1 If the currency symbol and sign string are adjacent, a <space>
+ * separates them from the value; otherwise, a <space> separates
+ * the currency symbol from the value.
+ * 2 If the currency symbol and sign string are adjacent, a <space>
+ * separates them; otherwise, a <space> separates the sign string
+ * from the value.
+ *----------
+ */
+ switch (sign_posn)
{
- result = palloc(CASH_BUFSZ + 2 - count);
- strcpy(result, buf + count);
+ case 0:
+ if (cs_precedes)
+ result = psprintf("(%s%s%s)",
+ csymbol,
+ (sep_by_space == 1) ? " " : "",
+ bufptr);
+ else
+ result = psprintf("(%s%s%s)",
+ bufptr,
+ (sep_by_space == 1) ? " " : "",
+ csymbol);
+ break;
+ case 1:
+ default:
+ if (cs_precedes)
+ result = psprintf("%s%s%s%s%s",
+ signsymbol,
+ (sep_by_space == 2) ? " " : "",
+ csymbol,
+ (sep_by_space == 1) ? " " : "",
+ bufptr);
+ else
+ result = psprintf("%s%s%s%s%s",
+ signsymbol,
+ (sep_by_space == 2) ? " " : "",
+ bufptr,
+ (sep_by_space == 1) ? " " : "",
+ csymbol);
+ break;
+ case 2:
+ if (cs_precedes)
+ result = psprintf("%s%s%s%s%s",
+ csymbol,
+ (sep_by_space == 1) ? " " : "",
+ bufptr,
+ (sep_by_space == 2) ? " " : "",
+ signsymbol);
+ else
+ result = psprintf("%s%s%s%s%s",
+ bufptr,
+ (sep_by_space == 1) ? " " : "",
+ csymbol,
+ (sep_by_space == 2) ? " " : "",
+ signsymbol);
+ break;
+ case 3:
+ if (cs_precedes)
+ result = psprintf("%s%s%s%s%s",
+ signsymbol,
+ (sep_by_space == 2) ? " " : "",
+ csymbol,
+ (sep_by_space == 1) ? " " : "",
+ bufptr);
+ else
+ result = psprintf("%s%s%s%s%s",
+ bufptr,
+ (sep_by_space == 1) ? " " : "",
+ signsymbol,
+ (sep_by_space == 2) ? " " : "",
+ csymbol);
+ break;
+ case 4:
+ if (cs_precedes)
+ result = psprintf("%s%s%s%s%s",
+ csymbol,
+ (sep_by_space == 2) ? " " : "",
+ signsymbol,
+ (sep_by_space == 1) ? " " : "",
+ bufptr);
+ else
+ result = psprintf("%s%s%s%s%s",
+ bufptr,
+ (sep_by_space == 1) ? " " : "",
+ csymbol,
+ (sep_by_space == 2) ? " " : "",
+ signsymbol);
+ break;
}
PG_RETURN_CSTRING(result);
}
/* cash_words()
- * This converts a int4 as well but to a representation using words
+ * This converts an int4 as well but to a representation using words
* Obviously way North American centric - sorry
*/
Datum
PG_RETURN_CASH(result);
}
+
+/* int4_cash()
+ * Convert int4 (int) to cash
+ */
+Datum
+int4_cash(PG_FUNCTION_ARGS)
+{
+ int32 amount = PG_GETARG_INT32(0);
+ Cash result;
+ int fpoint;
+ int64 scale;
+ int i;
+ struct lconv *lconvert = PGLC_localeconv();
+
+ /* see comments about frac_digits in cash_in() */
+ fpoint = lconvert->frac_digits;
+ if (fpoint < 0 || fpoint > 10)
+ fpoint = 2;
+
+ /* compute required scale factor */
+ scale = 1;
+ for (i = 0; i < fpoint; i++)
+ scale *= 10;
+
+ /* compute amount * scale, checking for overflow */
+ result = DatumGetInt64(DirectFunctionCall2(int8mul, Int64GetDatum(amount),
+ Int64GetDatum(scale)));
+
+ PG_RETURN_CASH(result);
+}
+
+/* int8_cash()
+ * Convert int8 (bigint) to cash
+ */
+Datum
+int8_cash(PG_FUNCTION_ARGS)
+{
+ int64 amount = PG_GETARG_INT64(0);
+ Cash result;
+ int fpoint;
+ int64 scale;
+ int i;
+ struct lconv *lconvert = PGLC_localeconv();
+
+ /* see comments about frac_digits in cash_in() */
+ fpoint = lconvert->frac_digits;
+ if (fpoint < 0 || fpoint > 10)
+ fpoint = 2;
+
+ /* compute required scale factor */
+ scale = 1;
+ for (i = 0; i < fpoint; i++)
+ scale *= 10;
+
+ /* compute amount * scale, checking for overflow */
+ result = DatumGetInt64(DirectFunctionCall2(int8mul, Int64GetDatum(amount),
+ Int64GetDatum(scale)));
+
+ PG_RETURN_CASH(result);
+}