postgresql/src/backend/utils/adt/cash.c

767 lines
15 KiB
C

/*
* cash.c
* Written by D'Arcy J.M. Cain
*
* Functions to allow input and output of money normally but store
* and handle it as int4s
*
* A slightly modified version of this file and a discussion of the
* workings can be found in the book "Software Solutions in C" by
* Dale Schumacher, Academic Press, ISBN: 0-12-632360-7.
*
* $Header: /cvsroot/pgsql/src/backend/utils/adt/cash.c,v 1.45 2000/08/03 16:34:22 tgl Exp $
*/
#include <limits.h>
#include <ctype.h>
#include <math.h>
#include <locale.h>
#include "postgres.h"
#include "miscadmin.h"
#include "utils/builtins.h"
#include "utils/cash.h"
static const char *num_word(Cash value);
/* when we go to 64 bit values we will have to modify this */
#define CASH_BUFSZ 24
#define TERMINATOR (CASH_BUFSZ - 1)
#define LAST_PAREN (TERMINATOR - 1)
#define LAST_DIGIT (LAST_PAREN - 1)
#ifdef USE_LOCALE
static struct lconv *lconvert = NULL;
#endif
/*
* Cash is a pass-by-ref SQL type, so we must pass and return pointers.
* These macros and support routine hide the pass-by-refness.
*/
#define PG_GETARG_CASH(n) (* ((Cash *) PG_GETARG_POINTER(n)))
#define PG_RETURN_CASH(x) return CashGetDatum(x)
static Datum
CashGetDatum(Cash value)
{
Cash *result = (Cash *) palloc(sizeof(Cash));
*result = value;
return PointerGetDatum(result);
}
/* cash_in()
* Convert a string to a cash data type.
* Format is [$]###[,]###[.##]
* Examples: 123.45 $123.45 $123,456.78
*
* This is currently implemented as a 32-bit integer.
* XXX HACK It looks as though some of the symbols for
* monetary values returned by localeconv() can be multiple
* bytes/characters. This code assumes one byte only. - tgl 97/04/14
* XXX UNHACK Allow the currency symbol to be multi-byte.
* - thomas 1998-03-01
*/
Datum
cash_in(PG_FUNCTION_ARGS)
{
char *str = PG_GETARG_CSTRING(0);
Cash result;
Cash value = 0;
Cash dec = 0;
Cash sgn = 1;
int seen_dot = 0;
const char *s = str;
int fpoint;
char *csymbol;
char dsymbol,
ssymbol,
psymbol,
*nsymbol;
#ifdef USE_LOCALE
#ifdef CASHDEBUG
setlocale(LC_ALL, "");
lconvert = localeconv();
#endif
if (lconvert == NULL)
lconvert = localeconv();
/* frac_digits in the C locale seems to return CHAR_MAX */
/* best guess is 2 in this case I think */
fpoint = ((lconvert->frac_digits != (char)CHAR_MAX) ? lconvert->frac_digits : 2); /* int_frac_digits? */
dsymbol = ((*lconvert->mon_decimal_point != '\0') ? *lconvert->mon_decimal_point : '.');
ssymbol = ((*lconvert->mon_thousands_sep != '\0') ? *lconvert->mon_thousands_sep : ',');
csymbol = ((*lconvert->currency_symbol != '\0') ? lconvert->currency_symbol : "$");
psymbol = ((*lconvert->positive_sign != '\0') ? *lconvert->positive_sign : '+');
nsymbol = ((*lconvert->negative_sign != '\0') ? lconvert->negative_sign : "-");
#else
fpoint = 2;
dsymbol = '.';
ssymbol = ',';
csymbol = "$";
psymbol = '+';
nsymbol = "-";
#endif
#ifdef CASHDEBUG
printf("cashin- precision '%d'; decimal '%c'; thousands '%c'; currency '%s'; positive '%c'; negative '%s'\n",
fpoint, dsymbol, ssymbol, csymbol, psymbol, nsymbol);
#endif
/* we need to add all sorts of checking here. For now just */
/* strip all leading whitespace and any leading currency symbol */
while (isspace((int) *s))
s++;
if (strncmp(s, csymbol, strlen(csymbol)) == 0)
s += strlen(csymbol);
#ifdef CASHDEBUG
printf("cashin- string is '%s'\n", s);
#endif
/* a leading minus or paren signifies a negative number */
/* again, better heuristics needed */
if (strncmp(s, nsymbol, strlen(nsymbol)) == 0)
{
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++;
#ifdef CASHDEBUG
printf("cashin- string is '%s'\n", s);
#endif
while (isspace((int) *s))
s++;
if (strncmp(s, csymbol, strlen(csymbol)) == 0)
s += strlen(csymbol);
#ifdef CASHDEBUG
printf("cashin- string is '%s'\n", s);
#endif
for (;; s++)
{
/* we look for digits as int4 as we have less */
/* than the required number of decimal places */
if (isdigit((int) *s) && dec < fpoint)
{
value = (value * 10) + *s - '0';
if (seen_dot)
dec++;
/* decimal point? then start counting fractions... */
}
else if (*s == dsymbol && !seen_dot)
{
seen_dot = 1;
/* "thousands" separator? then skip... */
}
else if (*s == ssymbol)
{
}
else
{
/* round off */
if (isdigit((int) *s) && *s >= '5')
value++;
/* adjust for less than required decimal places */
for (; dec < fpoint; dec++)
value *= 10;
break;
}
}
while (isspace((int) *s) || *s == '0' || *s == ')')
s++;
if (*s != '\0')
elog(ERROR, "Bad money external representation %s", str);
result = (value * sgn);
#ifdef CASHDEBUG
printf("cashin- result is %d\n", result);
#endif
PG_RETURN_CASH(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
*/
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 comma_position = 0;
char mon_group,
comma,
points;
char *csymbol,
dsymbol,
*nsymbol;
char convention;
#ifdef USE_LOCALE
if (lconvert == NULL)
lconvert = localeconv();
mon_group = *lconvert->mon_grouping;
comma = ((*lconvert->mon_thousands_sep != '\0') ? *lconvert->mon_thousands_sep : ',');
/* frac_digits in the C locale seems to return CHAR_MAX */
/* best guess is 2 in this case I think */
points = ((lconvert->frac_digits != (char)CHAR_MAX) ? lconvert->frac_digits : 2); /* int_frac_digits? */
convention = lconvert->n_sign_posn;
dsymbol = ((*lconvert->mon_decimal_point != '\0') ? *lconvert->mon_decimal_point : '.');
csymbol = ((*lconvert->currency_symbol != '\0') ? lconvert->currency_symbol : "$");
nsymbol = ((*lconvert->negative_sign != '\0') ? lconvert->negative_sign : "-");
#else
mon_group = 3;
comma = ',';
csymbol = "$";
dsymbol = '.';
nsymbol = "-";
points = 2;
convention = 0;
#endif
point_pos = LAST_DIGIT - points;
/* We're playing a little fast and loose with this. Shoot me. */
/* Not me, that was the other guy. Haven't fixed it yet - thomas */
if (!mon_group || mon_group == (char)CHAR_MAX)
mon_group = 3;
/* allow more than three decimal points and separate them */
if (comma)
{
point_pos -= (points - 1) / mon_group;
comma_position = point_pos % (mon_group + 1);
}
/* we work with positive amounts and add the minus sign at the end */
if (value < 0)
{
minus = 1;
value *= -1;
}
/* allow for trailing negative strings */
MemSet(buf, ' ', CASH_BUFSZ);
buf[TERMINATOR] = buf[LAST_PAREN] = '\0';
while (value || count > (point_pos - 2))
{
if (points && count == point_pos)
buf[count--] = dsymbol;
else if (comma && count % (mon_group + 1) == comma_position)
buf[count--] = comma;
buf[count--] = (value % 10) + '0';
value /= 10;
}
strncpy((buf + count - strlen(csymbol) + 1), csymbol, strlen(csymbol));
count -= strlen(csymbol) - 1;
if (buf[LAST_DIGIT] == ',')
buf[LAST_DIGIT] = buf[LAST_PAREN];
/* see if we need to signify negative amount */
if (minus)
{
if (!PointerIsValid(result = palloc(CASH_BUFSZ + 2 - count + strlen(nsymbol))))
elog(ERROR, "Memory allocation failed, can't output cash");
/* 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
{
if (!PointerIsValid(result = palloc(CASH_BUFSZ + 2 - count)))
elog(ERROR, "Memory allocation failed, can't output cash");
strcpy(result, buf + count);
}
PG_RETURN_CSTRING(result);
}
Datum
cash_eq(PG_FUNCTION_ARGS)
{
Cash c1 = PG_GETARG_CASH(0);
Cash c2 = PG_GETARG_CASH(1);
PG_RETURN_BOOL(c1 == c2);
}
Datum
cash_ne(PG_FUNCTION_ARGS)
{
Cash c1 = PG_GETARG_CASH(0);
Cash c2 = PG_GETARG_CASH(1);
PG_RETURN_BOOL(c1 != c2);
}
Datum
cash_lt(PG_FUNCTION_ARGS)
{
Cash c1 = PG_GETARG_CASH(0);
Cash c2 = PG_GETARG_CASH(1);
PG_RETURN_BOOL(c1 < c2);
}
Datum
cash_le(PG_FUNCTION_ARGS)
{
Cash c1 = PG_GETARG_CASH(0);
Cash c2 = PG_GETARG_CASH(1);
PG_RETURN_BOOL(c1 <= c2);
}
Datum
cash_gt(PG_FUNCTION_ARGS)
{
Cash c1 = PG_GETARG_CASH(0);
Cash c2 = PG_GETARG_CASH(1);
PG_RETURN_BOOL(c1 > c2);
}
Datum
cash_ge(PG_FUNCTION_ARGS)
{
Cash c1 = PG_GETARG_CASH(0);
Cash c2 = PG_GETARG_CASH(1);
PG_RETURN_BOOL(c1 >= c2);
}
/* cash_pl()
* Add two cash values.
*/
Datum
cash_pl(PG_FUNCTION_ARGS)
{
Cash c1 = PG_GETARG_CASH(0);
Cash c2 = PG_GETARG_CASH(1);
Cash result;
result = c1 + c2;
PG_RETURN_CASH(result);
}
/* cash_mi()
* Subtract two cash values.
*/
Datum
cash_mi(PG_FUNCTION_ARGS)
{
Cash c1 = PG_GETARG_CASH(0);
Cash c2 = PG_GETARG_CASH(1);
Cash result;
result = c1 - c2;
PG_RETURN_CASH(result);
}
/* cash_mul_flt8()
* Multiply cash by float8.
*/
Datum
cash_mul_flt8(PG_FUNCTION_ARGS)
{
Cash c = PG_GETARG_CASH(0);
float8 f = PG_GETARG_FLOAT8(1);
Cash result;
result = c * f;
PG_RETURN_CASH(result);
}
/* flt8_mul_cash()
* Multiply float8 by cash.
*/
Datum
flt8_mul_cash(PG_FUNCTION_ARGS)
{
float8 f = PG_GETARG_FLOAT8(0);
Cash c = PG_GETARG_CASH(1);
Cash result;
result = f * c;
PG_RETURN_CASH(result);
}
/* cash_div_flt8()
* Divide cash by float8.
*
* XXX Don't know if rounding or truncating is correct behavior.
* Round for now. - tgl 97/04/15
*/
Datum
cash_div_flt8(PG_FUNCTION_ARGS)
{
Cash c = PG_GETARG_CASH(0);
float8 f = PG_GETARG_FLOAT8(1);
Cash result;
if (f == 0.0)
elog(ERROR, "cash_div: divide by 0.0 error");
result = rint(c / f);
PG_RETURN_CASH(result);
}
/* cash_mul_flt4()
* Multiply cash by float4.
*/
Datum
cash_mul_flt4(PG_FUNCTION_ARGS)
{
Cash c = PG_GETARG_CASH(0);
float4 f = PG_GETARG_FLOAT4(1);
Cash result;
result = c * f;
PG_RETURN_CASH(result);
}
/* flt4_mul_cash()
* Multiply float4 by cash.
*/
Datum
flt4_mul_cash(PG_FUNCTION_ARGS)
{
float4 f = PG_GETARG_FLOAT4(0);
Cash c = PG_GETARG_CASH(1);
Cash result;
result = f * c;
PG_RETURN_CASH(result);
}
/* cash_div_flt4()
* Divide cash by float4.
*
* XXX Don't know if rounding or truncating is correct behavior.
* Round for now. - tgl 97/04/15
*/
Datum
cash_div_flt4(PG_FUNCTION_ARGS)
{
Cash c = PG_GETARG_CASH(0);
float4 f = PG_GETARG_FLOAT4(1);
Cash result;
if (f == 0.0)
elog(ERROR, "cash_div: divide by 0.0 error");
result = rint(c / f);
PG_RETURN_CASH(result);
}
/* cash_mul_int4()
* Multiply cash by int4.
*/
Datum
cash_mul_int4(PG_FUNCTION_ARGS)
{
Cash c = PG_GETARG_CASH(0);
int32 i = PG_GETARG_INT32(1);
Cash result;
result = c * i;
PG_RETURN_CASH(result);
}
/* int4_mul_cash()
* Multiply int4 by cash.
*/
Datum
int4_mul_cash(PG_FUNCTION_ARGS)
{
int32 i = PG_GETARG_INT32(0);
Cash c = PG_GETARG_CASH(1);
Cash result;
result = i * c;
PG_RETURN_CASH(result);
}
/* cash_div_int4()
* Divide cash by 4-byte integer.
*
* XXX Don't know if rounding or truncating is correct behavior.
* Round for now. - tgl 97/04/15
*/
Datum
cash_div_int4(PG_FUNCTION_ARGS)
{
Cash c = PG_GETARG_CASH(0);
int32 i = PG_GETARG_INT32(1);
Cash result;
if (i == 0)
elog(ERROR, "cash_div_int4: divide by 0 error");
result = rint(c / i);
PG_RETURN_CASH(result);
}
/* cash_mul_int2()
* Multiply cash by int2.
*/
Datum
cash_mul_int2(PG_FUNCTION_ARGS)
{
Cash c = PG_GETARG_CASH(0);
int16 s = PG_GETARG_INT16(1);
Cash result;
result = c * s;
PG_RETURN_CASH(result);
}
/* int2_mul_cash()
* Multiply int2 by cash.
*/
Datum
int2_mul_cash(PG_FUNCTION_ARGS)
{
int16 s = PG_GETARG_INT16(0);
Cash c = PG_GETARG_CASH(1);
Cash result;
result = s * c;
PG_RETURN_CASH(result);
}
/* cash_div_int2()
* Divide cash by int2.
*
* XXX Don't know if rounding or truncating is correct behavior.
* Round for now. - tgl 97/04/15
*/
Datum
cash_div_int2(PG_FUNCTION_ARGS)
{
Cash c = PG_GETARG_CASH(0);
int16 s = PG_GETARG_INT16(1);
Cash result;
if (s == 0)
elog(ERROR, "cash_div: divide by 0 error");
result = rint(c / s);
PG_RETURN_CASH(result);
}
/* cashlarger()
* Return larger of two cash values.
*/
Datum
cashlarger(PG_FUNCTION_ARGS)
{
Cash c1 = PG_GETARG_CASH(0);
Cash c2 = PG_GETARG_CASH(1);
Cash result;
result = (c1 > c2) ? c1 : c2;
PG_RETURN_CASH(result);
}
/* cashsmaller()
* Return smaller of two cash values.
*/
Datum
cashsmaller(PG_FUNCTION_ARGS)
{
Cash c1 = PG_GETARG_CASH(0);
Cash c2 = PG_GETARG_CASH(1);
Cash result;
result = (c1 < c2) ? c1 : c2;
PG_RETURN_CASH(result);
}
/* cash_words()
* This converts a int4 as well but to a representation using words
* Obviously way North American centric - sorry
*/
Datum
cash_words(PG_FUNCTION_ARGS)
{
Cash value = PG_GETARG_CASH(0);
char buf[128];
char *p = buf;
Cash m0;
Cash m1;
Cash m2;
Cash m3;
text *result;
/* work with positive numbers */
if (value < 0)
{
value = -value;
strcpy(buf, "minus ");
p += 6;
}
else
buf[0] = '\0';
m0 = value % 100; /* cents */
m1 = (value / 100) % 1000; /* hundreds */
m2 = (value / 100000) % 1000; /* thousands */
m3 = value / 100000000 % 1000; /* millions */
if (m3)
{
strcat(buf, num_word(m3));
strcat(buf, " million ");
}
if (m2)
{
strcat(buf, num_word(m2));
strcat(buf, " thousand ");
}
if (m1)
strcat(buf, num_word(m1));
if (!*p)
strcat(buf, "zero");
strcat(buf, (int) (value / 100) == 1 ? " dollar and " : " dollars and ");
strcat(buf, num_word(m0));
strcat(buf, m0 == 1 ? " cent" : " cents");
/* capitalize output */
buf[0] = toupper(buf[0]);
/* make a text type for output */
result = (text *) palloc(strlen(buf) + VARHDRSZ);
VARATT_SIZEP(result) = strlen(buf) + VARHDRSZ;
memcpy(VARDATA(result), buf, strlen(buf));
PG_RETURN_TEXT_P(result);
}
/*************************************************************************
* Private routines
************************************************************************/
static const char *
num_word(Cash value)
{
static char buf[128];
static const char *small[] = {
"zero", "one", "two", "three", "four", "five", "six", "seven",
"eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen",
"fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty",
"thirty", "fourty", "fifty", "sixty", "seventy", "eighty", "ninety"
};
const char **big = small + 18;
int tu = value % 100;
/* deal with the simple cases first */
if (value <= 20)
return small[value];
/* is it an even multiple of 100? */
if (!tu)
{
sprintf(buf, "%s hundred", small[value / 100]);
return buf;
}
/* more than 99? */
if (value > 99)
{
/* is it an even multiple of 10 other than 10? */
if (value % 10 == 0 && tu > 10)
sprintf(buf, "%s hundred %s",
small[value / 100], big[tu / 10]);
else if (tu < 20)
sprintf(buf, "%s hundred and %s",
small[value / 100], small[tu]);
else
sprintf(buf, "%s hundred %s %s",
small[value / 100], big[tu / 10], small[tu % 10]);
}
else
{
/* is it an even multiple of 10 other than 10? */
if (value % 10 == 0 && tu > 10)
sprintf(buf, "%s", big[tu / 10]);
else if (tu < 20)
sprintf(buf, "%s", small[tu]);
else
sprintf(buf, "%s %s", big[tu / 10], small[tu % 10]);
}
return buf;
} /* num_word() */