pspp-dev
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[no subject]


From: John Darrington
Subject:
Date: Mon, 12 Jan 2015 20:08:19 +0100

>From cf20af4f1e18dd2005c6aa666ffe0b104cb20eb2 Mon Sep 17 00:00:00 2001
From: John Darrington <address@hidden>
Date: Tue, 6 Jan 2015 07:25:21 +0100
Subject: [PATCH] New module to perform decimal floating point arithmetic for
 charts.

This change adds a small module to perform floating point arithmetic
with a decimal base, and uses it to calculate the graticule marks for
charts.

Rationale:  Graticule marks want to be displayed in decimal.  However
not all decimal values can be represented in a double precision
floating point binary.  This approach pushes the precision loss from
the value of the mark, to the position on the chart - we don't
particularly care if a tick mark is a fraction of a pixel out of place,
but we do care if 4.0 is displayed as 3.9999999999
---
 src/math/automake.mk                               |    1 +
 src/math/chart-geometry.c                          |  130 ++++-
 src/math/chart-geometry.h                          |    7 +-
 src/math/decimal.c                                 |  572 ++++++++++++++++++++
 src/math/decimal.h                                 |  115 ++++
 src/math/histogram.c                               |   15 +-
 src/output/cairo-chart.c                           |   48 +-
 src/output/cairo-chart.h                           |    4 +-
 src/output/charts/boxplot-cairo.c                  |    2 +-
 src/output/charts/np-plot-cairo.c                  |    8 +-
 src/output/charts/plot-hist-cairo.c                |   21 +-
 src/output/charts/roc-chart-cairo.c                |    4 +-
 src/output/charts/scatterplot-cairo.c              |    4 +-
 src/output/charts/scree-cairo.c                    |    4 +-
 src/output/charts/spreadlevel-cairo.c              |    4 +-
 tests/automake.mk                                  |   31 ++
 .../math/chart-geometry-test.c                     |   72 +--
 tests/math/chart-geometry.at                       |   35 ++
 tests/math/chart-get-scale-test.c                  |   96 ++++
 tests/math/decimal-test.c                          |  310 +++++++++++
 tests/math/decimal.at                              |    7 +
 21 files changed, 1394 insertions(+), 96 deletions(-)
 create mode 100644 src/math/decimal.c
 create mode 100644 src/math/decimal.h
 copy src/math/chart-geometry.c => tests/math/chart-geometry-test.c (50%)
 create mode 100644 tests/math/chart-geometry.at
 create mode 100644 tests/math/chart-get-scale-test.c
 create mode 100644 tests/math/decimal-test.c
 create mode 100644 tests/math/decimal.at

diff --git a/src/math/automake.mk b/src/math/automake.mk
index cdcc2c8..f40aac5 100644
--- a/src/math/automake.mk
+++ b/src/math/automake.mk
@@ -17,6 +17,7 @@ src_math_libpspp_math_la_SOURCES = \
        src/math/covariance.h \
        src/math/correlation.c \
        src/math/correlation.h \
+       src/math/decimal.c src/math/decimal.h \
        src/math/extrema.c src/math/extrema.h \
        src/math/histogram.c src/math/histogram.h \
        src/math/interaction.c src/math/interaction.h \
diff --git a/src/math/chart-geometry.c b/src/math/chart-geometry.c
index eb0ad92..35380a6 100644
--- a/src/math/chart-geometry.c
+++ b/src/math/chart-geometry.c
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2004 Free Software Foundation, Inc.
+   Copyright (C) 2004, 2015 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
@@ -17,41 +17,135 @@
 #include <config.h>
 #include <math.h>
 #include <float.h>
+#include <assert.h>
 
 #include "chart-geometry.h"
+#include "decimal.h"
+#include <stdlib.h>
 
-static const double standard_ticks[] = {1, 2, 5, 10};
-
+static const double standard_tick[] = {1, 2, 5, 10};
 
 /* Adjust tick to be a sensible value
    ie:  ... 0.1,0.2,0.5,   1,2,5,  10,20,50 ... */
-double
-chart_rounded_tick (double tick)
+void
+chart_rounded_tick (double tick, struct decimal *result)
 {
   int i;
 
-  double diff = DBL_MAX;
-  double t = tick;
-
-  double factor;
+  struct decimal ddif = {1, 1000};
 
   /* Avoid arithmetic problems with very small values */
   if (fabs (tick) < DBL_EPSILON)
-     return 0;
+    {
+      result->ordinate = 0;
+      result->mantissa = 0;
+      return;
+    }
 
-  factor = pow (10,ceil (log10 (standard_ticks[0] / tick)));
+  struct decimal dt;
+  decimal_from_double (&dt, tick);
+  
+  double expd = dec_log10 (&dt) - 1;
 
-  for (i = 3  ; i >= 0 ; --i)
+  for (i = 0  ; i < 4 ; ++i)
     {
-      const double d = fabs (tick - standard_ticks[i] / factor);
+      struct decimal candidate;
+      struct decimal delta;
 
-      if ( d < diff )
+      decimal_init (&candidate, standard_tick[i], expd);
+      
+      delta = dt;
+      decimal_subtract (&delta, &candidate);
+      delta.ordinate = llabs (delta.ordinate);
+
+      if (decimal_cmp (&delta, &ddif) < 0)
        {
-         diff = d;
-         t = standard_ticks[i] / factor ;
+         ddif = delta;
+         *result = candidate;
        }
     }
-
-  return t;
 }
 
+/* 
+   Find a set {LOWER, INTERVAL, N_TICKS} such that:
+
+   LOWER <= LOWDBL,
+   LOWER + INTERVAL > LOWDBL,
+   
+   LOWER + N_TICKS * INTERVAL < HIGHDBL
+   LOWER + (N_TICKS + 1) * INTERVAL >= HIGHDBL
+
+   INTERVAL = X * 10^N
+    where: N is integer 
+    and    X is an element of {1, 2, 5}
+
+   In other words:
+
+         INTERVAL
+         >      <
+     |....+....+....+.      .+....|
+   LOWER  1    2    3     N_TICKS
+        ^LOWDBL                 ^HIGHDBL
+*/
+void
+chart_get_scale (double highdbl, double lowdbl,
+                struct decimal *lower, 
+                struct decimal *interval,
+                int *n_ticks)
+{
+  int i;
+  double fitness = DBL_MAX;
+  *n_ticks = 0;
+  struct decimal high;
+  struct decimal low;
+
+  assert (highdbl >= lowdbl);
+
+  decimal_from_double (&high, highdbl);
+  decimal_from_double (&low, lowdbl);
+  
+  struct decimal diff = high;
+  decimal_subtract (&diff, &low);
+
+  double expd = dec_log10 (&diff) - 2;
+
+  /* Find the most pleasing interval */
+  for (i = 1; i < 4; ++i)
+    {
+      struct decimal clbound = low;
+      struct decimal cubound = high;
+      struct decimal candidate;
+      decimal_init (&candidate, standard_tick[i], expd);
+
+      decimal_divide (&clbound, &candidate);
+      int fl = decimal_floor (&clbound);
+      decimal_int_multiply (&candidate, fl);
+      clbound = candidate;
+
+
+      decimal_init (&candidate, standard_tick[i], expd);
+      decimal_divide (&cubound, &candidate);
+      int fu = decimal_ceil (&cubound);
+      decimal_int_multiply (&candidate, fu);
+
+      cubound = candidate;
+
+      decimal_init (&candidate, standard_tick[i], expd);
+      decimal_subtract (&cubound, &clbound);
+      decimal_divide (&cubound, &candidate);
+
+
+      ord_t n_int = decimal_floor (&cubound);
+
+      /* We prefer to have between 5 and 10 tick marks on a scale */
+      double f = 1 - exp (-0.2 *  fabs (n_int - 7.5) / 7.5);
+
+      if (f < fitness)
+       {
+         fitness = f;
+         *lower = clbound;
+         *interval = candidate;
+         *n_ticks = n_int;
+       }
+    }
+}
diff --git a/src/math/chart-geometry.h b/src/math/chart-geometry.h
index ef481fe..c543086 100644
--- a/src/math/chart-geometry.h
+++ b/src/math/chart-geometry.h
@@ -18,6 +18,11 @@
 #ifndef CHART_GEOMETRY_H
 #define CHART_GEOMETRY_H
 
-double chart_rounded_tick(double tick);
+struct decimal;
+void chart_rounded_tick(double tick, struct decimal *);
+
+void chart_get_scale (double high, double low,
+                          struct decimal *lower, struct decimal *interval, int 
*n_ticks);
+
 
 #endif
diff --git a/src/math/decimal.c b/src/math/decimal.c
new file mode 100644
index 0000000..ba33e76
--- /dev/null
+++ b/src/math/decimal.c
@@ -0,0 +1,572 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2015 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program 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 General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+
+#include <config.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <limits.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <math.h>
+#include "libpspp/i18n.h"
+
+#include "decimal.h"
+
+int dec_warning;
+
+static bool
+down (struct decimal *dec)
+{
+  if (dec->ordinate % 10 == 0 &&  dec->mantissa < MANT_MAX - 1)
+    {
+      dec->ordinate /= 10;
+      dec->mantissa++;
+      return true;
+    }
+  
+  return false;
+}
+
+static bool
+up (struct decimal *dec)
+{
+  if (llabs (dec->ordinate) < ORD_MAX / 10   &&   dec->mantissa > MANT_MIN)
+    {
+      dec->ordinate *= 10;
+      dec->mantissa--;
+      return true;
+    }
+  return false;
+}
+
+
+/* Reduce the absolute value of the ordinate to the smallest possible,
+   without loosing precision */
+static void 
+reduce (struct decimal *dec)
+{
+  if (dec->ordinate == 0)
+    {
+      dec->mantissa = 0;
+      return;
+    }
+    
+  while (dec->ordinate % 10 == 0)
+    {
+      if (! down (dec))
+       break;
+    }
+}
+
+/* Attempt to make the mantissas of BM and SM equal.
+   Prerequisite: the mantissa SM must be no greater than that of BM.
+ */
+static void 
+normalisebs (struct decimal *sm, struct decimal *bm)
+{
+  while (sm->mantissa < bm->mantissa)
+    {
+      if (down (sm))
+       ;
+      else if (up (bm))
+       ;
+      else
+       {
+         dec_warning = DEC_PREC;
+         break;
+       }
+    }
+
+  while (sm->mantissa < bm->mantissa)
+    {
+      sm->ordinate /= 10;
+      sm->mantissa++;
+    }
+}
+
+
+/* arrange d1 and d2 such that thier mantissas are equal */
+void 
+normalise (struct decimal *d1, struct decimal *d2)
+{
+  normalisebs (d1, d2);
+  normalisebs (d2, d1);
+}
+
+
+
+/* Return log base 10 of D */
+mant_t 
+dec_log10 (const struct decimal *d_)
+{
+  struct decimal d = *d_;
+
+  while (llabs (d.ordinate) > 0)
+    {
+      d.ordinate /= 10;
+      d.mantissa++;
+    }
+
+  return d.mantissa;
+}
+
+
+
+/* Return the smallest integer >= d */
+static ord_t
+decimal_ceil_pos (const struct decimal *d)
+{
+  mant_t m = d->mantissa;
+  ord_t o = d->ordinate;
+
+  assert (d->ordinate >= 0);
+  
+  while (m > 0)
+    {
+      o *= 10;
+      m--;
+    }
+
+  while (m < 0)
+    {
+      bool flag = o % 10;
+      o /= 10;
+      if (flag)
+       o++;
+      m++;
+    }
+
+  return o;
+}
+
+/* Return the largest integer <= d */
+static ord_t
+decimal_floor_pos (const struct decimal *d)
+{
+  mant_t m = d->mantissa;
+  ord_t o = d->ordinate;
+
+  assert (d->ordinate >= 0);
+
+  while (m > 0)
+    {
+      m--;
+      o *= 10;
+    }
+
+  while (m < 0)
+    {
+      m++;
+      o /= 10;
+    }
+  
+
+  return o;
+}
+
+/* Return the smallest integer which is no less than D.
+   (round towards minus infinity) */
+ord_t
+decimal_floor (const struct decimal *d)
+{
+  if (d->ordinate >= 0)
+    return decimal_floor_pos (d);
+  else
+    {
+      struct decimal dd = *d;
+      dd.ordinate = llabs (dd.ordinate);
+      return -decimal_ceil_pos (&dd);
+    }
+}
+
+/* Return the largest integer which is no greater than D.
+   (round towards plus infinity) */
+ord_t
+decimal_ceil (const struct decimal *d)
+{
+  if (d->ordinate >= 0)
+    return decimal_ceil_pos (d);
+  else
+    {
+      struct decimal dd = *d;
+      dd.ordinate = llabs (dd.ordinate);
+      return -decimal_floor_pos (&dd);
+    }
+}
+
+/* Add SRC onto DEST */
+void
+decimal_add (struct decimal *dest, const struct decimal *src_)
+{
+  struct decimal src = *src_;
+
+  src.ordinate = -src.ordinate;
+
+  decimal_subtract (dest, &src);
+}
+
+/* Subtract SRC from DEST */
+void
+decimal_subtract (struct decimal *dest, const struct decimal *src_)
+{
+  struct decimal src = *src_;
+
+  normalise (dest, &src);
+
+  bool dest_neg = dest->ordinate < 0;
+  bool src_neg = src.ordinate < 0;
+
+  bool expected_neg = dest_neg * src_neg;
+  
+  if (dest->ordinate == src.ordinate)
+    {
+      expected_neg = 0;
+    }
+  else if (llabs (src.ordinate) > llabs (dest->ordinate))
+    {
+      if (dest_neg == src_neg)
+       expected_neg = !expected_neg;
+    }
+
+  dest->ordinate -= src.ordinate;
+
+  bool result_neg = dest->ordinate < 0;
+
+  if (expected_neg != result_neg)
+    {
+      /* The operation has resulted in an overflow.
+        To resolve this, undo the operation, 
+        reduce the precision and try again */
+
+      dest->ordinate += src.ordinate;
+
+      dest->ordinate /= 10;
+      src.ordinate /= 10;
+
+      dest->mantissa ++;
+      src.mantissa ++;
+
+      dest->ordinate -= src.ordinate;
+    }
+
+  reduce (dest);
+
+}
+
+/* Initialise DEC with ordinate ORD and mantissa MANT */
+void
+decimal_init (struct decimal *dec, ord_t ord, mant_t mant)
+{
+  dec->ordinate = ord;
+  dec->mantissa = mant;
+  reduce (dec);
+}
+
+
+/*
+  Compare D1 and D2.
+
+  Returns zero if equal, +1 if D1 > D2 and -1 if D1 < D2
+*/
+int
+decimal_cmp (const struct decimal *d1, const struct decimal *d2)
+{
+  struct decimal td1 = *d1;
+  struct decimal td2 = *d2;
+  
+  normalise (&td1, &td2);
+
+  if (td1.ordinate < td2.ordinate)
+    return -1;
+
+  return (td1.ordinate > td2.ordinate);
+}
+
+
+/* Multiply DEST by SRC */
+void
+decimal_multiply (struct decimal *dest, const struct decimal *src)
+{
+  dest->ordinate *= src->ordinate;
+  dest->mantissa += src->mantissa;
+}
+
+
+/* Multiply DEST by M */
+void
+decimal_int_multiply (struct decimal *dest, ord_t m)
+{
+  dest->ordinate *= m;
+  reduce (dest);
+}
+
+/* Divide DEST by M */
+void
+decimal_int_divide (struct decimal *dest, ord_t m)
+{
+  while (dest->ordinate % m)
+    {
+      if (labs (dest->ordinate) > ORD_MAX / 10)
+       {
+         dec_warning = DEC_PREC;
+         break;
+       }
+      up (dest);
+    }
+  dest->ordinate /= m;
+}
+
+/* Divide DEST by SRC */
+void
+decimal_divide (struct decimal *dest, const struct decimal *src)
+{
+  while (dest->ordinate % src->ordinate)
+    {
+      if (labs (dest->ordinate) > ORD_MAX / 10)
+       {
+         dec_warning = DEC_PREC;
+         break;
+       }
+      up (dest);
+    }
+
+  dest->ordinate /= src->ordinate;
+  dest->mantissa -= src->mantissa;
+}
+
+/* Print the value of DEC to F.  Probably useful only for debugging */
+void
+decimal_show (const struct decimal *dec, FILE *f)
+{
+  fprintf (f, PR_ORD " x 10^" PR_MANT "\n", dec->ordinate, dec->mantissa);
+}
+
+
+/* Reverse the characters in string S which has length LEN */
+static void 
+reverse (char *s, int len)
+{
+  int i;
+  for (i = 0; i < len / 2; ++i)
+    {
+      char temp = s[len - i - 1];
+      s[len - i - 1] = s[i];
+      s[i] = temp;
+    }
+}
+
+/* Return a string representation of DEC on the heap.
+   The caller is responsible for freeing the string */
+char *
+decimal_to_string (const struct decimal *dec)
+{
+  int cap = 16;
+  int len = 0;
+  char *s = calloc (cap, 1);
+  ord_t ordinate = dec->ordinate;
+
+  while (len < dec->mantissa)
+    {
+      s[len++] = '0';
+      if (len >= cap) s = realloc (s, cap <<= 1);
+    }
+
+  while (ordinate)
+    {
+      s[len++] = labs (ordinate % 10) + '0';
+      if (len >= cap) s = realloc (s, cap <<= 1);
+      ordinate /= 10;
+    }
+
+  if (ordinate < 0)
+      ordinate = -ordinate;
+
+  while (len < -dec->mantissa)
+    {
+      s[len++] = '0';
+      if (len >= cap) s = realloc (s, cap <<= 1);
+    }
+
+  if (dec->mantissa < 0 )
+    {
+      if (len <= -dec->mantissa)
+       {
+         s[len++] = get_system_decimal ();
+         if (len >= cap) s = realloc (s, cap <<= 1);
+         s[len++] = '0';
+         if (len >= cap) s = realloc (s, cap <<= 1);
+       }
+      else
+       {
+         int i;
+         if (len >= cap) s = realloc (s, cap <<= 1);
+         for (i = len - 1 ; i >= -dec->mantissa ; --i)
+           s[i + 1] = s[i];
+         s[i + 1] = get_system_decimal ();
+         len++;
+       }
+    }
+
+  if (dec->ordinate < 0)
+    {
+      s[len++] = '-';
+      if (len >= cap) s = realloc (s, cap <<= 1);
+    }
+
+
+  reverse (s, len);
+
+  {
+    int abs_len = len;
+    if (dec->ordinate < 0)
+      abs_len--;
+
+    while (abs_len++ <= dec->mantissa)
+      {
+       s[len++] = '0';
+       if (len >= cap) s = realloc (s, cap <<= 1);
+      }
+  }
+  
+  return s;
+}
+
+
+/* Initialise DECIMAL from INPUT.
+   INPUT should be a convential decimal representation.
+ */
+void
+decimal_init_from_string (struct decimal *decimal, const char *input)
+{
+  ord_t ordinate = 0;
+
+  int point = -1;
+  int lsd = -1;
+  int fsd = -1;
+  int i = 0;
+  int len = 0;
+  int sign = 1;
+
+  const char *p;
+
+  for (p = input; *p ; p++)
+    {
+      if (*p == '-')
+       {
+         sign = -1;
+       }
+      else if (*p == get_system_decimal ())
+       {
+         assert (point == -1);
+         point = i;
+       }
+      else if (*p > '0' && *p <= '9')
+       {
+         lsd = i;
+         if (fsd == -1)
+           fsd = i;
+       }
+      else if (*p == '0')
+       /* ignore */
+       ;
+      else 
+       {
+         fprintf (stderr, "Error: invalid character %c\n", *p);
+         return;
+       }
+
+      i++;
+    }
+  len = i;
+
+  if (point == -1)
+    point = len;
+
+  mant_t mantissa = 0;
+  if (fsd >= 0)
+    {
+      mant_t m = 1;
+      for (i = lsd ; i >= fsd ; --i)
+       {
+         if (input[i] != get_system_decimal ())
+           {
+             if (ordinate > ORD_MAX - m * (input[i] - '0'))
+               {
+                 fprintf (stderr, "Overflow reading value %s\n", input);
+                 break;
+               }
+             ordinate += m * (input[i] - '0');
+             m *= 10;
+           }
+       }
+
+      if (lsd > point)
+       mantissa = point - lsd;
+      else
+       mantissa = point - lsd - 1;
+    }
+
+  decimal->ordinate = ordinate * sign;
+  decimal->mantissa = mantissa;
+}
+
+
+
+/* Initialise DEC from the binary fp value X */
+void 
+decimal_from_double (struct decimal *dec, double x)
+{
+  dec->mantissa = 0;
+
+  while (trunc (x) != x)
+    {
+      if (fabs (x) > ORD_MAX / 10.0)
+       {
+         dec_warning = DEC_PREC;
+         break;
+       }
+      x *= 10.0;
+      dec->mantissa--;
+    }
+
+  dec->ordinate = x;
+}
+
+/* Return a binary floating point value 
+   approximating DEC */
+double
+decimal_to_double (const struct decimal *dec)
+{
+  double x = dec->ordinate;
+  int mult = dec->mantissa;
+
+  while (mult < 0)
+    {
+      x /= 10.0;
+      mult++;
+    }
+
+  while (mult > 0)
+    {
+      x *= 10.0;
+      mult--;
+    }
+
+  return x;
+}
diff --git a/src/math/decimal.h b/src/math/decimal.h
new file mode 100644
index 0000000..2a3fd60
--- /dev/null
+++ b/src/math/decimal.h
@@ -0,0 +1,115 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2015 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program 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 General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+
+#ifndef DECIMAL_H
+#define DECIMAL_H
+
+/* This module provides a rudimentary floating point implementation using a 
decimal
+   base.   It can be used for floating point calculations where it is 
desirable that
+   the result is representable in decimal base.
+
+   Any of the functions may set the static variable dec_warning to non-zero if 
a
+   loss of precision or other issue occurs.
+
+   This does not purport to be efficient, either in time or space.
+ */
+
+#include <stdio.h>
+#include <stdbool.h>
+
+#include <limits.h>
+
+#define DEC_PREC 1 /* operation resulted in a loss of precision */
+extern int dec_warning;
+
+
+#define ORDINATE_LONG
+#define MANTISSA_LONG
+
+#ifdef ORDINATE_SHORT
+typedef short ord_t;
+static const short ORD_MAX = SHRT_MAX;
+#define PR_ORD "%d"
+#endif
+
+#ifdef ORDINATE_INT
+typedef int ord_t;
+static const int ORD_MAX = INT_MAX;
+#define PR_ORD "%d"
+#endif
+
+#ifdef ORDINATE_LONG
+typedef long ord_t;
+static const long ORD_MAX = LONG_MAX;
+#define PR_ORD "%ld"
+#endif
+
+
+
+#ifdef MANTISSA_SHORT
+typedef short mant_t;
+static const short MANT_MAX = SHRT_MAX;
+#define PR_MANT "%d"
+#endif
+
+#ifdef MANTISSA_INT
+typedef int mant_t;
+static const int MANT_MAX = INT_MAX;
+#define PR_MANT "%d"
+#endif
+
+#ifdef MANTISSA_LONG
+typedef long mant_t;
+static const long MANT_MAX = LONG_MAX;
+#define PR_MANT "%ld"
+#endif
+
+
+
+#define MANT_MIN       (-MANT_MAX - 1)
+#define ORD_MIN                (-ORD_MAX - 1)
+
+struct decimal 
+{
+  ord_t ordinate;
+  mant_t mantissa;
+};
+
+void normalise (struct decimal *d1, struct decimal *d2);
+void decimal_init (struct decimal *dec, ord_t ord, mant_t mant);
+void decimal_init_from_string (struct decimal *dec, const char *s);
+int decimal_cmp (const struct decimal *d1, const struct decimal *d2);
+void decimal_multiply (struct decimal *dest, const struct decimal *src);
+void decimal_int_multiply (struct decimal *dest, ord_t m);
+void decimal_int_divide (struct decimal *dest, ord_t m);
+void decimal_divide (struct decimal *dest, const struct decimal *src);
+void decimal_show (const struct decimal *dec, FILE *f);
+char *decimal_to_string (const struct decimal *dec);
+
+void decimal_add (struct decimal *dest, const struct decimal *);
+void decimal_subtract (struct decimal *dest, const struct decimal *);
+ord_t decimal_ceil (const struct decimal *d);
+ord_t decimal_floor (const struct decimal *d);
+mant_t dec_log10 (const struct decimal *d);
+
+
+void decimal_from_double (struct decimal *dec, double x);
+double decimal_to_double (const struct decimal *dec);
+
+
+
+#endif
diff --git a/src/math/histogram.c b/src/math/histogram.c
index b000672..3c324ef 100644
--- a/src/math/histogram.c
+++ b/src/math/histogram.c
@@ -17,6 +17,7 @@
 #include <config.h>
 
 #include "math/histogram.h"
+#include "math/decimal.h"
 
 #include <gsl/gsl_histogram.h>
 #include <math.h>
@@ -211,8 +212,9 @@ adjust_bin_ranges (double bin_width, double min, double 
max, double *adj_min, do
 
 
 struct histogram *
-histogram_create (double bin_width, double min, double max)
+histogram_create (double bin_width_in, double min, double max)
 {
+  struct decimal bin_width;
   const int MAX_BINS = 25;
   struct histogram *h;
   struct statistic *stat;
@@ -225,16 +227,17 @@ histogram_create (double bin_width, double min, double 
max)
       return NULL;
     }
 
-  assert (bin_width > 0);
+  assert (bin_width_in > 0);
 
-  bin_width = chart_rounded_tick (bin_width);
-  bins = adjust_bin_ranges (bin_width, min, max, &adjusted_min, &adjusted_max);
+  chart_rounded_tick (bin_width_in, &bin_width);
+  bins = adjust_bin_ranges (decimal_to_double (&bin_width), 
+                           min, max, &adjusted_min, &adjusted_max);
 
   /* Force the number of bins to lie in a sensible range. */
   if (bins > MAX_BINS) 
     {
-      bin_width = chart_rounded_tick ((max - min) / (double) (MAX_BINS - 1));
-      bins = adjust_bin_ranges (bin_width,
+      chart_rounded_tick ((max - min) / (double) (MAX_BINS - 1), &bin_width);
+      bins = adjust_bin_ranges (decimal_to_double (&bin_width),
                                 min, max, &adjusted_min, &adjusted_max);
     }
 
diff --git a/src/output/cairo-chart.c b/src/output/cairo-chart.c
index 8886b26..01c5d5c 100644
--- a/src/output/cairo-chart.c
+++ b/src/output/cairo-chart.c
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2004, 2009, 2010, 2011, 2014 Free Software Foundation, Inc.
+   Copyright (C) 2004, 2009, 2010, 2011, 2014, 2015 Free Software Foundation, 
Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
@@ -17,6 +17,8 @@
 #include <config.h>
 
 #include "output/cairo-chart.h"
+#include "math/decimal.h"
+#include "math/chart-geometry.h"
 
 #include <assert.h>
 #include <cairo/cairo.h>
@@ -347,45 +349,57 @@ xrchart_write_title (cairo_t *cr, const struct 
xrchart_geometry *geom,
 
 static void
 xrchart_write_scale (cairo_t *cr, struct xrchart_geometry *geom,
-                    double smin, double smax, int ticks, enum tick_orientation 
orient)
+                    double smin, double smax, enum tick_orientation orient)
 {
   int s;
+  int ticks;
 
-  const double tick_interval =
-    chart_rounded_tick ((smax - smin) / (double) ticks);
+  struct decimal dinterval;
+  struct decimal dlower;
+  struct decimal dupper;
 
-  int upper = ceil (smax / tick_interval);
-  int lower = floor (smin / tick_interval);
+  chart_get_scale (smax, smin, &dlower, &dinterval, &ticks);
 
-  geom->axis[orient].max = tick_interval * upper;
-  geom->axis[orient].min = tick_interval * lower;
+  dupper = dinterval;
+  decimal_int_multiply (&dupper, ticks);
+  decimal_add (&dupper, &dlower);
 
+  double tick_interval = decimal_to_double (&dinterval);
+   
+  geom->axis[orient].max = decimal_to_double (&dupper);
+  geom->axis[orient].min = decimal_to_double (&dlower);
+  
   geom->axis[orient].scale = (fabs (geom->axis[orient].data_max - 
geom->axis[orient].data_min)
-     / fabs (geom->axis[orient].max - geom->axis[orient].min));
+                             / fabs (geom->axis[orient].max - 
geom->axis[orient].min));
+  
+  struct decimal pos = dlower;
 
-  for (s = 0 ; s < upper - lower; ++s)
+  for (s = 0 ; s < ticks; ++s)
     {
-      double pos = (s + lower) * tick_interval;
+      char *str = decimal_to_string (&pos);
       draw_tick (cr, geom, orient, false,
-                s * tick_interval * geom->axis[orient].scale, "%.*g",
-                 DBL_DIG + 1, pos);
+                s * tick_interval * geom->axis[orient].scale,
+                "%s", str);
+      free (str);
+      
+      decimal_add (&pos, &dinterval);
     }
 }
 
 /* Set the scale for the ordinate */
 void
 xrchart_write_yscale (cairo_t *cr, struct xrchart_geometry *geom,
-                    double smin, double smax, int ticks)
+                    double smin, double smax)
 {
-  xrchart_write_scale (cr, geom, smin, smax, ticks, SCALE_ORDINATE);
+  xrchart_write_scale (cr, geom, smin, smax, SCALE_ORDINATE);
 }
 
 /* Set the scale for the abscissa */
 void
 xrchart_write_xscale (cairo_t *cr, struct xrchart_geometry *geom,
-                    double smin, double smax, int ticks)
+                     double smin, double smax)
 {
-  xrchart_write_scale (cr, geom, smin, smax, ticks, SCALE_ABSCISSA);
+  xrchart_write_scale (cr, geom, smin, smax, SCALE_ABSCISSA);
 }
 
 
diff --git a/src/output/cairo-chart.h b/src/output/cairo-chart.h
index edf4ed1..27000cb 100644
--- a/src/output/cairo-chart.h
+++ b/src/output/cairo-chart.h
@@ -118,12 +118,12 @@ void xrchart_write_title (cairo_t *, const struct 
xrchart_geometry *,
 
 /* Set the scale for the abscissa */
 void xrchart_write_xscale (cairo_t *, struct xrchart_geometry *,
-                           double min, double max, int ticks);
+                           double min, double max);
 
 
 /* Set the scale for the ordinate */
 void xrchart_write_yscale (cairo_t *, struct xrchart_geometry *,
-                           double smin, double smax, int ticks);
+                           double smin, double smax);
 
 void xrchart_write_xlabel (cairo_t *, const struct xrchart_geometry *,
                            const char *label) ;
diff --git a/src/output/charts/boxplot-cairo.c 
b/src/output/charts/boxplot-cairo.c
index 946b7d4..a35b277 100644
--- a/src/output/charts/boxplot-cairo.c
+++ b/src/output/charts/boxplot-cairo.c
@@ -143,7 +143,7 @@ xrchart_draw_boxplot (const struct chart_item *chart_item, 
cairo_t *cr,
   double box_width;
   size_t i;
 
-  xrchart_write_yscale (cr, geom, boxplot->y_min, boxplot->y_max, 5);
+  xrchart_write_yscale (cr, geom, boxplot->y_min, boxplot->y_max);
   xrchart_write_title (cr, geom, "%s", chart_item->title);
 
   box_width = (geom->axis[SCALE_ABSCISSA].data_max - 
geom->axis[SCALE_ABSCISSA].data_min) / boxplot->n_boxes / 2.0;
diff --git a/src/output/charts/np-plot-cairo.c 
b/src/output/charts/np-plot-cairo.c
index 334284b..e9a1e07 100644
--- a/src/output/charts/np-plot-cairo.c
+++ b/src/output/charts/np-plot-cairo.c
@@ -39,8 +39,8 @@ np_plot_chart_draw (const struct chart_item *chart_item, 
cairo_t *cr,
   xrchart_write_ylabel (cr, geom, _("Expected Normal"));
   xrchart_write_xscale (cr, geom,
                       npp->x_lower - npp->slack,
-                      npp->x_upper + npp->slack, 5);
-  xrchart_write_yscale (cr, geom, npp->y_first, npp->y_last, 5);
+                      npp->x_upper + npp->slack);
+  xrchart_write_yscale (cr, geom, npp->y_first, npp->y_last);
 
   data = casereader_clone (npp->data);
   for (; (c = casereader_read (data)) != NULL; case_unref (c))
@@ -64,8 +64,8 @@ dnp_plot_chart_draw (const struct chart_item *chart_item, 
cairo_t *cr,
   xrchart_write_title (cr, geom, _("Detrended Normal Q-Q Plot of %s"), 
chart_item->title);
   xrchart_write_xlabel (cr, geom, _("Observed Value"));
   xrchart_write_ylabel (cr, geom, _("Dev from Normal"));
-  xrchart_write_xscale (cr, geom, dnpp->y_min, dnpp->y_max, 5);
-  xrchart_write_yscale (cr, geom, dnpp->dns_min, dnpp->dns_max, 5);
+  xrchart_write_xscale (cr, geom, dnpp->y_min, dnpp->y_max);
+  xrchart_write_yscale (cr, geom, dnpp->dns_min, dnpp->dns_max);
 
   data = casereader_clone (dnpp->data);
   for (; (c = casereader_read (data)) != NULL; case_unref (c))
diff --git a/src/output/charts/plot-hist-cairo.c 
b/src/output/charts/plot-hist-cairo.c
index 93133c2..006b392 100644
--- a/src/output/charts/plot-hist-cairo.c
+++ b/src/output/charts/plot-hist-cairo.c
@@ -15,7 +15,7 @@
    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
 #include <config.h>
-
+#include "math/decimal.h"
 #include "output/charts/plot-hist.h"
 
 #include <float.h>
@@ -103,9 +103,20 @@ hist_draw_bar (cairo_t *cr, const struct xrchart_geometry 
*geom,
   cairo_stroke (cr);
 
   if (label)
-    draw_tick (cr, geom, SCALE_ABSCISSA, bins > 10,
-              x_pos + width / 2.0, "%.*g",
-               DBL_DIG + 1, (upper + lower) / 2.0);
+    {
+      struct decimal decupper;
+      struct decimal declower;
+      struct decimal middle;
+      decimal_from_double (&declower, lower);
+      decimal_from_double (&decupper, upper);
+      middle = declower;
+      decimal_add (&middle, &decupper);
+      decimal_int_divide (&middle, 2);
+      char *str = decimal_to_string (&middle);
+      draw_tick (cr, geom, SCALE_ABSCISSA, bins > 10,
+                x_pos + width / 2.0, "%s", str);
+      free (str);
+    }
 }
 
 void
@@ -129,7 +140,7 @@ xrchart_draw_histogram (const struct chart_item 
*chart_item, cairo_t *cr,
 
   bins = gsl_histogram_bins (h->gsl_hist);
 
-  xrchart_write_yscale (cr, geom, 0, gsl_histogram_max_val (h->gsl_hist), 5);
+  xrchart_write_yscale (cr, geom, 0, gsl_histogram_max_val (h->gsl_hist));
 
   for (i = 0; i < bins; i++)
     {
diff --git a/src/output/charts/roc-chart-cairo.c 
b/src/output/charts/roc-chart-cairo.c
index f6da350..2ac494e 100644
--- a/src/output/charts/roc-chart-cairo.c
+++ b/src/output/charts/roc-chart-cairo.c
@@ -37,8 +37,8 @@ xrchart_draw_roc (const struct chart_item *chart_item, 
cairo_t *cr,
   xrchart_write_xlabel (cr, geom, _("1 - Specificity"));
   xrchart_write_ylabel (cr, geom, _("Sensitivity"));
 
-  xrchart_write_xscale (cr, geom, 0, 1, 5);
-  xrchart_write_yscale (cr, geom, 0, 1, 5);
+  xrchart_write_xscale (cr, geom, 0, 1);
+  xrchart_write_yscale (cr, geom, 0, 1);
 
   if ( rc->reference )
     {
diff --git a/src/output/charts/scatterplot-cairo.c 
b/src/output/charts/scatterplot-cairo.c
index b555a12..f25b2cd 100644
--- a/src/output/charts/scatterplot-cairo.c
+++ b/src/output/charts/scatterplot-cairo.c
@@ -51,8 +51,8 @@ xrchart_draw_scatterplot (const struct chart_item 
*chart_item, cairo_t *cr,
 
   xrchart_write_xscale (cr, geom,
                       spc->x_min,
-                      spc->x_max, 5);
-  xrchart_write_yscale (cr, geom, spc->y_min, spc->y_max, 5);
+                      spc->x_max);
+  xrchart_write_yscale (cr, geom, spc->y_min, spc->y_max);
   xrchart_write_title (cr, geom, _("Scatterplot %s"), chart_item->title);
   xrchart_write_xlabel (cr, geom, var_to_string(spc->xvar));
   xrchart_write_ylabel (cr, geom, var_to_string(spc->yvar));
diff --git a/src/output/charts/scree-cairo.c b/src/output/charts/scree-cairo.c
index f9765c6..db0ce13 100644
--- a/src/output/charts/scree-cairo.c
+++ b/src/output/charts/scree-cairo.c
@@ -44,8 +44,8 @@ xrchart_draw_scree (const struct chart_item *chart_item, 
cairo_t *cr,
   else
     max = fabs (min);
 
-  xrchart_write_yscale (cr, geom, 0, max, max);
-  xrchart_write_xscale (cr, geom, 0, rc->eval->size + 1, rc->eval->size + 1);
+  xrchart_write_yscale (cr, geom, 0, max);
+  xrchart_write_xscale (cr, geom, 0, rc->eval->size + 1);
 
   xrchart_vector_start (cr, geom, "");
   for (i = 0 ; i < rc->eval->size; ++i)
diff --git a/src/output/charts/spreadlevel-cairo.c 
b/src/output/charts/spreadlevel-cairo.c
index eeb3c81..13b7a7a 100644
--- a/src/output/charts/spreadlevel-cairo.c
+++ b/src/output/charts/spreadlevel-cairo.c
@@ -39,8 +39,8 @@ xrchart_draw_spreadlevel (const struct chart_item 
*chart_item, cairo_t *cr,
   xrchart_write_ylabel (cr, geom, _("Spread"));
   
 
-  xrchart_write_xscale (cr, geom, sl->x_lower, sl->x_upper, 5);
-  xrchart_write_yscale (cr, geom, sl->y_lower, sl->y_upper, 5);
+  xrchart_write_xscale (cr, geom, sl->x_lower, sl->x_upper);
+  xrchart_write_yscale (cr, geom, sl->y_lower, sl->y_upper);
 
   for (i = 0 ; i < sl->n_data; ++i)
     {
diff --git a/tests/automake.mk b/tests/automake.mk
index 31a7877..4cb4283 100644
--- a/tests/automake.mk
+++ b/tests/automake.mk
@@ -31,6 +31,9 @@ check_PROGRAMS += \
        tests/libpspp/tower-test \
        tests/libpspp/u8-istream-test \
        tests/libpspp/zip-test \
+       tests/math/chart-geometry-test \
+       tests/math/chart-get-scale-test \
+       tests/math/decimal-test \
        tests/output/render-test \
        tests/ui/syntax-gen-test
 
@@ -209,6 +212,32 @@ tests_libpspp_zip_test_LDADD = \
        src/libpspp-core.la \
        gl/libgl.la 
 
+check_PROGRAMS += tests/math/chart-geometry-test
+tests_math_chart_geometry_test_SOURCES = tests/math/chart-geometry-test.c
+tests_math_chart_geometry_test_LDADD = \
+       src/math/libpspp-math.la \
+       src/libpspp/liblibpspp.la \
+       src/libpspp-core.la \
+       gl/libgl.la 
+
+check_PROGRAMS += tests/math/chart-get-scale-test
+tests_math_chart_get_scale_test_SOURCES = tests/math/chart-get-scale-test.c
+tests_math_chart_get_scale_test_LDADD = \
+       src/math/libpspp-math.la \
+       src/libpspp/liblibpspp.la \
+       src/libpspp-core.la \
+       gl/libgl.la 
+
+
+check_PROGRAMS += tests/math/decimal-test
+tests_math_decimal_test_SOURCES = tests/math/decimal-test.c
+tests_math_decimal_test_LDADD = \
+       src/math/libpspp-math.la \
+       src/libpspp/liblibpspp.la \
+       src/libpspp-core.la \
+       gl/libgl.la 
+
+
 check_PROGRAMS += tests/output/render-test
 tests_output_render_test_SOURCES = tests/output/render-test.c
 tests_output_render_test_LDADD = \
@@ -367,6 +396,8 @@ TESTSUITE_AT = \
        tests/libpspp/tower.at \
        tests/libpspp/u8-istream.at \
        tests/libpspp/zip.at \
+       tests/math/chart-geometry.at \
+       tests/math/decimal.at \
        tests/math/moments.at \
        tests/math/randist.at \
        tests/output/ascii.at \
diff --git a/src/math/chart-geometry.c b/tests/math/chart-geometry-test.c
similarity index 50%
copy from src/math/chart-geometry.c
copy to tests/math/chart-geometry-test.c
index eb0ad92..a713cdb 100644
--- a/src/math/chart-geometry.c
+++ b/tests/math/chart-geometry-test.c
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2004 Free Software Foundation, Inc.
+   Copyright (C) 2015 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
@@ -15,43 +15,47 @@
    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
 #include <config.h>
-#include <math.h>
-#include <float.h>
-
-#include "chart-geometry.h"
-
-static const double standard_ticks[] = {1, 2, 5, 10};
-
-
-/* Adjust tick to be a sensible value
-   ie:  ... 0.1,0.2,0.5,   1,2,5,  10,20,50 ... */
-double
-chart_rounded_tick (double tick)
+#include <stdlib.h>
+#include "math/chart-geometry.h"
+#include "math/decimal.h"
+
+
+const double in[20] =
+  {
+    0.00648687,
+    728815,
+    8.14431e-07,
+    77611.4,
+    3.33497,
+    180.426,
+    0.676168,
+    2.00744e+08,
+    14099.3,
+    19.5186,
+    1.17473e-07,
+    166337,
+    0.00163644,
+    1.94724e-09,
+    2.31564e-06,
+    3.10674e+06,
+    5.10314e-05,
+    1.95101,
+    1.40884e+09,
+    78217.6
+  };
+
+int 
+main ()
 {
   int i;
-
-  double diff = DBL_MAX;
-  double t = tick;
-
-  double factor;
-
-  /* Avoid arithmetic problems with very small values */
-  if (fabs (tick) < DBL_EPSILON)
-     return 0;
-
-  factor = pow (10,ceil (log10 (standard_ticks[0] / tick)));
-
-  for (i = 3  ; i >= 0 ; --i)
+  for (i = 0; i < 20; ++i)
     {
-      const double d = fabs (tick - standard_ticks[i] / factor);
-
-      if ( d < diff )
-       {
-         diff = d;
-         t = standard_ticks[i] / factor ;
-       }
+      struct decimal dout;
+      chart_rounded_tick (in[i], &dout);
+      
+      printf ("%g %s\n", in[i], decimal_to_string (&dout));
     }
 
-  return t;
+  return 0;
 }
 
diff --git a/tests/math/chart-geometry.at b/tests/math/chart-geometry.at
new file mode 100644
index 0000000..14e1584
--- /dev/null
+++ b/tests/math/chart-geometry.at
@@ -0,0 +1,35 @@
+AT_BANNER([Chart Geometry])
+
+AT_SETUP([Chart Rounding])
+
+AT_CHECK([../../math/chart-geometry-test], [0], [dnl
+0.00648687 0.005
+728815 500000
+8.14431e-07 0.000001
+77611.4 100000
+3.33497 2
+180.426 200
+0.676168 0.5
+2.00744e+08 200000000
+14099.3 10000
+19.5186 20
+1.17473e-07 0.0000001
+166337 200000
+0.00163644 0.002
+1.94724e-09 0.000000002
+2.31564e-06 0.000002
+3.10674e+06 2000000
+5.10314e-05 0.00005
+1.95101 2
+1.40884e+09 1000000000
+78217.6 100000
+])
+
+AT_CLEANUP
+
+
+AT_SETUP([Chart Scale])
+
+AT_CHECK([../../math/chart-get-scale-test], [0], [ignore])
+
+AT_CLEANUP
diff --git a/tests/math/chart-get-scale-test.c 
b/tests/math/chart-get-scale-test.c
new file mode 100644
index 0000000..11a572d
--- /dev/null
+++ b/tests/math/chart-get-scale-test.c
@@ -0,0 +1,96 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2015 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program 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 General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <string.h>
+
+#include "math/decimal.h"
+#include <limits.h>
+#include <float.h>
+#include <math.h>
+
+void
+dump_scale (const struct decimal *low, const struct decimal *interval, int 
n_ticks)
+{
+  int i;
+  struct decimal tick = *low;
+  for (i = 0; i <= n_ticks; ++i)
+    {
+      printf ("Tick %d: %s (%g)\n", i, decimal_to_string (&tick), 
decimal_to_double (&tick));
+      decimal_add (&tick, interval);
+    }
+}
+
+
+void
+test_range (double low, double high)
+{
+  int n_ticks = 0;
+  struct decimal interval;
+  struct decimal lower;
+
+  
+  chart_get_scale (high, low,
+                  &lower, &interval, &n_ticks);
+
+  assert (n_ticks > 0);
+  assert (n_ticks < 12);
+
+  //  dump_scale (&lower, &interval, n_ticks);
+
+  assert (decimal_to_double (&lower) <= low);
+  
+  {
+    struct decimal  l = lower;
+    decimal_add (&l, &interval);
+    assert (decimal_to_double (&l) > low);
+  }
+
+  {
+    struct decimal  i = interval;
+
+    decimal_int_multiply (&i, n_ticks - 1);
+
+    decimal_add (&i, &lower);
+
+    /* i now contains the upper bound minus one tick */
+    assert (decimal_to_double (&i) < high);
+
+    decimal_add (&i, &interval);
+
+    assert (decimal_to_double (&i) >= high);
+  }
+
+}
+
+
+int 
+main (int argc, char **argv)
+{
+  test_range (0.2, 11);
+  test_range (-0.2, 11);
+  test_range (-10, 0.2);
+  test_range (-10, -0.2);
+
+  test_range (102, 50030); 
+  test_range (0.00102, 0.0050030); 
+
+  return 0;
+}
diff --git a/tests/math/decimal-test.c b/tests/math/decimal-test.c
new file mode 100644
index 0000000..aa4d244
--- /dev/null
+++ b/tests/math/decimal-test.c
@@ -0,0 +1,310 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2015 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program 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 General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <string.h>
+
+#include "math/decimal.h"
+#include <limits.h>
+#include <float.h>
+#include <math.h>
+
+/* Canonicalise a string  holding the decimal representation of a number.
+   For example, leading zeros left of the decimal point are removed, as are
+   trailing zeros to the right.
+
+   This function is used purely for testing, and need not and is not intended
+   to be efficient.
+ */
+char *
+canonicalise_string (const char *s)
+{
+  char *out;
+  char *dot = NULL;
+  bool negative = false;
+  char *p;
+  char *temp = malloc (strlen (s) + 3);
+  char *last_leading_zero = NULL;
+
+  /* Strip leading - if present */
+  if (*s == '-')
+    {
+      negative = true;
+      s++;
+    }
+  
+  strcpy (temp, "00");
+  strcat (temp, s);
+
+  char *first_trailing_zero = NULL;
+  char *significant_digit = NULL;
+  for (p = temp; *p; p++)
+    {
+      if (*p == '0' && dot == NULL && significant_digit == NULL)
+       last_leading_zero = p;
+
+      if (*p == '0' && first_trailing_zero == NULL)
+       first_trailing_zero = p;
+
+      if (*p == '.')
+       {
+         dot = p;
+         first_trailing_zero = NULL;
+       }
+
+      if (*p >= '1' && *p <= '9')
+       {
+         significant_digit = p;
+         first_trailing_zero = NULL;
+       }
+    }
+
+  if (first_trailing_zero && dot)
+    *first_trailing_zero = '\0';
+
+  if (last_leading_zero)
+    {
+      /* Strip leading zeros */
+      out = last_leading_zero + 1;
+
+      /* But if we now start with . put a zero back again */
+      if (dot == last_leading_zero + 1)
+       out--;
+    }
+
+
+  if (negative)
+    {
+      out--;
+      out[0] = '-';
+    }
+  
+  if (!significant_digit)
+    {
+      *out = '0';
+      *(out+1) = '\0';
+    }
+    
+
+  return out;
+}
+
+
+/* Tests both the decimal_to_string function, and the 
decimal_input_from_string 
+   function */
+void
+test_run (const char *input)
+  {
+    struct decimal test;
+    struct decimal number;
+    decimal_init_from_string (&number, input);
+
+    char *s = decimal_to_string (&number);
+    char *canon = canonicalise_string (input);
+    if (0 != strcmp (canon, s))
+      {
+       fprintf (stdout, "\"%s\" does not match \n\"%s\"\n", canon, s);
+       exit (1);
+      }
+
+    decimal_init_from_string (&test, s);
+    assert (0 == decimal_cmp (&test, &number));
+
+    free (s);
+  }
+
+
+void
+test_can (const char *in, const char *soll)
+{
+  char *ist = canonicalise_string (in);
+  if (0 != strcmp (soll, ist))
+    {
+      printf ("\"%s\" canonicalises to \"%s\" (should be \"%s\")\n", in, ist, 
soll);
+    }
+}
+
+
+void
+dump_scale (const struct decimal *low, const struct decimal *interval, int 
n_ticks)
+{
+  int i;
+  struct decimal tick = *interval;
+  printf ("Lowest: %s\n", decimal_to_string (low));
+  for (i = 0; i <= n_ticks; ++i)
+    {
+      printf ("Tick %d: %s (%g)\n", i, decimal_to_string (&tick), 
decimal_to_double (&tick));
+      decimal_add (&tick, interval);
+    }
+}
+
+
+
+void
+test_ceil (double x)
+{
+  struct decimal dx;
+  decimal_from_double (&dx, x);
+  int act = decimal_ceil (&dx);
+  int expected = ceil (x);
+  
+  assert (act == expected);
+}
+
+void
+test_floor (double x)
+{
+  struct decimal dx;
+  decimal_from_double (&dx, x);
+  int act = decimal_floor (&dx);
+  int expected = floor (x);
+  
+  assert (act == expected);
+}
+
+
+void
+test_addition (const struct decimal *one_, const struct decimal *two)
+{
+  struct decimal one = *one_;
+  double d1 = decimal_to_double (&one);
+  double d2 = decimal_to_double (two);
+  
+  decimal_add (&one, two);
+  
+  double dsum = decimal_to_double (&one);
+
+  char sdsum1[256];
+  char sdsum2[256];
+
+  snprintf (sdsum1, 256, "%s", decimal_to_string (&one));
+  snprintf (sdsum2, 256, "%g", dsum);
+
+  assert (strcmp (sdsum1, sdsum2) == 0);
+}
+
+void
+test_multiplication (const struct decimal *one_, const struct decimal *two)
+{
+  struct decimal one = *one_;
+  double d1 = decimal_to_double (&one);
+  double d2 = decimal_to_double (two);
+  
+  decimal_multiply (&one, two);
+  
+  double dprod = decimal_to_double (&one);
+  
+  assert (dprod == d1 * d2);
+}
+
+
+int 
+main (int argc, char **argv)
+{
+  /* Test that our canonicalise function works for all corner cases we
+     can think of. */
+
+  test_can ("500", "500");
+  test_can ("5", "5");
+  test_can ("-3", "-3");
+  test_can ("-3.001", "-3.001");
+  test_can ("-03.001", "-3.001");
+  test_can ("-.0301", "-0.0301");
+  test_can ("0314.09", "314.09");
+  test_can ("0314.090", "314.09");
+  test_can ("0314.0900340", "314.090034");
+  test_can ("0.0", "0");
+  test_can ("0.", "0");
+  test_can (".0", "0");
+  test_can ("-.1", "-0.1");
+  test_can (".090", "0.09");
+  test_can ("03410.098700", "3410.0987");
+  test_can ("-03410.098700", "-3410.0987");
+
+  /* Test the conversion functions */
+  test_run ("-90000");
+  test_run ("-3");
+  test_run ("50001");
+  test_run ("500");
+  test_run ("350");
+  test_run ("050");
+  test_run ("4");
+  test_run ("0");
+  test_run (".45");
+  test_run ("-.45");
+  test_run ("666666666");
+  test_run ("6000000000");
+  test_run ("0.000000005");
+  test_run ("0.00000000000000000000000000000000000000005");
+  test_run ("0.0234");
+  test_run ("0.234");
+  test_run ("-0123.45600");
+
+  test_ceil (5.21);
+  test_ceil (-4.32);
+  test_ceil (0);
+  test_ceil (0.0009);
+
+  test_floor (4.09);
+  test_floor (-4.09);
+  test_floor (0);
+  test_floor (0.004);
+
+
+  {
+    struct decimal high = {2, 0};
+    struct decimal low = {2, -1};
+
+    test_addition (&high, &low);
+  }
+
+
+  {
+    struct decimal high = {10, 0};
+    struct decimal low = {2, -1};
+
+    test_addition (&high, &low);
+  }
+
+
+  {
+    struct decimal high = {10, 0};
+    struct decimal low = {-2, -1};
+
+    test_addition (&high, &low);
+  }
+
+  {
+    struct decimal high = {12, -5};
+    struct decimal low = {-2, -1};
+
+    test_addition (&high, &low);
+  }
+
+  {
+    struct decimal high = {-112, -1};
+    struct decimal low = {2, -1};
+
+    test_addition (&high, &low);
+  }
+
+
+  return 0;
+}
diff --git a/tests/math/decimal.at b/tests/math/decimal.at
new file mode 100644
index 0000000..b3e71d1
--- /dev/null
+++ b/tests/math/decimal.at
@@ -0,0 +1,7 @@
+AT_BANNER([Decimal floating point])
+
+AT_SETUP([Decimal float])
+
+AT_CHECK([../../math/decimal-test], [0], [ignore])
+
+AT_CLEANUP
-- 
1.7.10.4




reply via email to

[Prev in Thread] Current Thread [Next in Thread]