001package org.fhir.ucum;
002
003/*******************************************************************************
004 * Crown Copyright (c) 2006 - 2014, Copyright (c) 2006 - 2014 Kestral Computing & Health Intersections P/L.
005 * All rights reserved. This program and the accompanying materials
006 * are made available under the terms of the Eclipse Public License v1.0
007 * which accompanies this distribution, and is available at
008 * http://www.eclipse.org/legal/epl-v10.html
009 * 
010 * Contributors:
011 *    Kestral Computing P/L - initial implementation (pascal)
012 *    Health Intersections P/L - port to Java
013 *******************************************************************************/
014
015/**
016    Precision aware Decimal implementation. Any size number with any number of significant digits is supported.
017
018    Note that operations are precision aware operations. Note that whole numbers are assumed to have
019    unlimited precision. For example:
020      2 x 2 = 4
021      2.0 x 2.0 = 4.0
022      2.00 x 2.0 = 4.0
023    and
024     10 / 3 = 3.33333333333333333333333333333333333333333333333
025     10.0 / 3 = 3.33
026     10.00 / 3 = 3.333
027     10.00 / 3.0 = 3.3
028     10 / 3.0 = 3.3
029
030    Addition
031      2 + 0.001 = 2.001
032      2.0 + 0.001 = 2.0
033
034    Note that the string representation is precision limited, but the internal representation
035    is not.
036
037    
038  * This class is defined to work around the limitations of Java Big Decimal
039 * 
040 * @author Grahame
041 *
042 */
043public class Decimal {
044
045        private int precision;
046        private boolean scientific;
047        private boolean negative;
048        private String digits;
049        private int decimal;
050
051        private Decimal() {
052                super();
053        }
054        
055        public Decimal(String value) throws UcumException  {
056                super();
057                value = value.toLowerCase();
058                if (value.contains("e"))
059                        setValueScientific(value);
060                else
061                        setValueDecimal(value);
062        }
063
064        /**
065         * There are a few circumstances where a simple value is known to be correct to a high
066         * precision. For instance, the unit prefix milli is not ~0.001, it is precisely 0.001
067         * to whatever precision you want to specify. This constructor allows you to specify 
068         * an alternative precision than the one implied by the stated string
069         * 
070         * @param value - a string representation of the value
071         * @param precision - a 
072         * @throws UcumException 
073         * @
074         */
075        public Decimal(String value, int precision) throws UcumException  {
076                super();
077                value = value.toLowerCase();
078                if (value.contains("e"))
079                        setValueScientific(value);
080                else
081                        setValueDecimal(value);
082                this.precision = precision;
083        }
084
085        public Decimal(int i) {
086                super();
087                try {
088            setValueDecimal(Integer.toString(i));
089    } catch (Exception e) {
090    }
091  }
092
093        private void setValueDecimal(String value) throws UcumException  {
094                //      var
095                //        dec : integer;
096                //        i : integer;
097                scientific = false;
098                int dec = -1;
099                negative = value.startsWith("-");
100                if (negative)
101                        value = value.substring(1);
102
103                while (value.startsWith("0") && value.length() > 1)
104                        value = value.substring(1);
105
106                for (int i = 0; i < value.length(); i++) {
107                        if (value.charAt(i) == '.' && dec == -1)
108                                dec = i;
109                        else if (!Character.isDigit(value.charAt(i)))
110                                throw new UcumException("'"+value+"' is not a valid decimal");
111                }
112
113                if (dec == -1) {
114                        precision = value.length();
115                        decimal = value.length();
116                        digits = value;
117                } else if (dec == value.length() -1)
118                        throw new UcumException("'"+value+"' is not a valid decimal");
119                else {
120                        decimal = dec;
121                        if (allZeros(value, 1))
122                                precision = value.length() - 1;
123                        else
124                                precision = countSignificants(value);
125                        digits = delete(value, decimal, 1);
126                        if (allZeros(digits, 0))
127                                precision++;
128                        else
129                                while (digits.charAt(0) == '0') {
130                                        digits = digits.substring(1);
131                                        decimal--;
132                                }
133                }
134        }
135
136        private boolean allZeros(String s, int start) {
137                boolean result = true;
138                for (int i = start; i < s.length(); i++) {
139                        if (s.charAt(i) != '0')
140                                result = false;
141                }
142                return result;
143        }
144
145        private int countSignificants(String value) {
146                int i = value.indexOf(".");
147                if (i > -1)
148                        value = delete(value, i, 1);
149                while (value.charAt(0) == '0')
150                        value = value.substring(1);
151                return value.length();
152        }
153
154        private String delete(String value, int offset, int length) {
155                if (offset == 0)
156                        return value.substring(length);
157                else
158                        return value.substring(0,  offset)+value.substring(offset+length);
159        }
160        
161        private void setValueScientific(String value) throws UcumException  {
162                int i = value.indexOf("e");
163                String s = value.substring(0, i);
164                String e = value.substring(i+1);
165                                
166          if (Utilities.noString(s) || s.equals("-") || !Utilities.isDecimal(s))
167            throw new UcumException("'"+value+"' is not a valid decimal (numeric)");
168          if (Utilities.noString(e) || e.equals("-") || !Utilities.isInteger(e))
169            throw new UcumException("'"+value+"' is not a valid decimal (exponent)");
170
171          setValueDecimal(s);
172          scientific = true;
173
174          // now adjust for exponent
175
176          if (e.charAt(0) == '-')
177            i = 1;
178          else
179            i = 0;
180          while (i < e.length()) {
181            if (!Character.isDigit(e.charAt(i)))
182              throw new UcumException(""+value+"' is not a valid decimal");
183            i++;
184          }
185          i = Integer.parseInt(e);
186          decimal = decimal + i;
187        }
188
189        private String stringMultiply(char c, int i) {
190          return Utilities.padLeft("", c, i);
191  }
192
193        private String insert(String ins, String value, int offset) {
194                if (offset == 0)
195                        return ins+value;
196                else
197                        return value.substring(0, offset)+ins+value.substring(offset);
198  }
199
200        @Override
201  public String toString() {
202          return asDecimal();
203  }
204
205        public Decimal copy() {
206                Decimal result = new Decimal();
207                result.precision = precision;
208                result.scientific = scientific;
209                result.negative = negative;
210                result.digits = digits;
211                result.decimal = decimal;
212                return result;
213        }
214
215        public static Decimal zero()  {
216                try {
217                        return new Decimal("0");
218                } catch (Exception e) {
219                        return null; // won't happen
220                }
221        }
222
223        public boolean isZero() {
224          return allZeros(digits, 0);
225        }
226        
227        public static Decimal one() {
228                try {
229                        return new Decimal("1");
230                } catch (Exception e) {
231                        return null; // won't happen
232                }
233        }
234
235        public boolean isOne() {
236          Decimal one = one();
237          return comparesTo(one) == 0;
238        }
239
240        public boolean equals(Decimal other) {
241                return comparesTo(other) == 0;
242        }
243        
244        public int comparesTo(Decimal other) {
245                //        s1, s2 : AnsiString;
246                if (other == null)
247                        return 0;
248
249                if (this.negative && !other.negative)
250                        return -1;
251                else if (!this.negative && other.negative)
252                        return 1;
253                else {
254                        int max = Math.max(this.decimal, other.decimal);
255                        String s1 = stringMultiply('0', max - this.decimal+1) + this.digits;
256                        String s2 = stringMultiply('0', max - other.decimal+1) + other.digits;
257                        if (s1.length() < s2.length()) 
258                                s1 = s1 + stringMultiply('0', s2.length() - s1.length());
259                                else if (s2.length() < s1.length()) 
260                                        s2 = s2 + stringMultiply('0', s1.length() - s2.length());
261                        int result = s1.compareTo(s2);
262                        if (this.negative)
263                                result = -result;
264                        return result;
265                }
266        }
267
268        public boolean isWholeNumber() {
269          return !asDecimal().contains(".");
270        }
271
272        public String asDecimal() {
273                String result = digits;
274                if (decimal != digits.length())
275                        if (decimal < 0)
276                                result = "0."+stringMultiply('0', 0-decimal)+digits;
277                        else if (decimal < result.length())
278                                if (decimal == 0) 
279                                        result = "0."+result;
280                                else
281                                        result = insert(".", result, decimal);
282                        else
283                                result = result + stringMultiply('0', decimal - result.length());
284                if (negative && !allZeros(result, 0))
285                        result = "-" + result;
286                return result;
287        }
288
289        public int asInteger() throws UcumException  {
290          if (!isWholeNumber())
291            throw new UcumException("Unable to represent "+toString()+" as an integer");
292          if (comparesTo(new Decimal(Integer.MIN_VALUE)) < 0)
293                throw new UcumException("Unable to represent "+toString()+" as a signed 8 byte integer");
294          if (comparesTo(new Decimal(Integer.MAX_VALUE)) > 0)
295                throw new UcumException("Unable to represent "+toString()+" as a signed 8 byte integer");
296          return Integer.parseInt(asDecimal());
297  }
298
299        public String asScientific() {
300          String result = digits;
301          boolean zero = allZeros(result, 0);
302          if (zero) {
303            if (precision < 2)
304              result = "0e0";
305            else
306              result = "0."+stringMultiply('0', precision-1)+"e0";
307          } else {
308            if (digits.length() > 1)
309              result = insert(".", result, 1);
310            result = result + 'e'+Integer.toString(decimal - 1);
311          }
312          if (negative && !zero)
313            result = '-' + result;
314          return result;
315  }
316
317        public Decimal trunc() {
318          if (decimal < 0)
319    return zero();
320
321    Decimal result = copy();
322    if (result.digits.length() >= result.decimal)
323        result.digits = result.digits.substring(0, result.decimal);
324    if (Utilities.noString(result.digits)) {
325      result.digits = "0";
326      result.decimal = 1;
327      result.negative = false;
328    }
329          return result;
330  }
331
332
333        public Decimal add(Decimal other) {
334          if (other == null) 
335            return null;
336          
337          if (negative == other.negative) {
338            Decimal result = doAdd(other);
339            result.negative = negative;
340            return result;
341          } else if (negative) 
342            return other.doSubtract(this);
343          else
344                return doSubtract(other);
345        }
346
347        public Decimal subtract(Decimal other) {
348          if (other == null) 
349            return null;
350
351          Decimal result;
352    if (negative && !other.negative) {
353            result = doAdd(other);
354            result.negative = true;
355    } else if (!negative && other.negative) {
356            result = doAdd(other);
357    } else if (negative && other.negative) {
358            result = doSubtract(other);
359            result.negative = !result.negative;
360    } else { 
361            result = other.doSubtract(this);
362            result.negative = !result.negative;
363    }
364    return result;
365        }
366
367        
368        private Decimal doAdd(Decimal other) {
369          int max = Math.max(decimal, other.decimal);
370          String s1 = stringMultiply('0', max - decimal+1) + digits;
371          String s2 = stringMultiply('0', max - other.decimal+1) + other.digits;
372          if (s1.length() < s2.length())
373            s1 = s1 + stringMultiply('0', s2.length() - s1.length());
374          else if (s2.length() < s1.length())
375            s2 = s2 + stringMultiply('0', s1.length() - s2.length());
376
377          String s3 = stringAddition(s1, s2);
378
379          if (s3.charAt(0) == '1')
380            max++;
381          else
382            s3 = delete(s3, 0, 1);
383          
384          if (max != s3.length()) {
385            if (max < 0)
386              throw new Error("Unhandled");
387            else if (max < s3.length())
388              s3 = insert(".", s3, max);
389            else
390              throw new Error("Unhandled");
391          }
392          
393          Decimal result = new Decimal();
394          try {
395            result.setValueDecimal(s3);
396    } catch (Exception e) {
397        // won't happen
398    }
399          result.scientific = scientific || other.scientific;
400          // todo: the problem with this is you have to figure out the absolute precision and take the lower of the two, not the relative one
401          if (decimal < other.decimal)
402            result.precision = precision;
403          else if (other.decimal < decimal)
404            result.precision = other.precision;
405          else
406            result.precision = Math.min(precision, other.precision);
407          return result;
408        }
409
410        private int dig(char c) {
411          return (c) - ('0');
412  }
413
414        private char cdig(int i) {
415          return (char) (i + ('0'));
416  }
417
418        private Decimal doSubtract(Decimal other)  {
419          int max = Math.max(decimal, other.decimal);
420          String s1 = stringMultiply('0', max - decimal+1) + digits;
421          String s2 = stringMultiply('0', max - other.decimal+1) + other.digits;
422          if (s1.length() < s2.length())
423            s1 = s1 + stringMultiply('0', s2.length() - s1.length());
424          else if (s2.length() < s1.length())
425            s2 = s2 + stringMultiply('0', s1.length() - s2.length());
426
427          String s3;
428          boolean neg = (s1.compareTo(s2) <  0);
429          if (neg) {
430            s3 = s2;
431            s2 = s1;
432            s1 = s3;
433          }
434
435          s3 = stringSubtraction(s1, s2);
436
437          if (s3.charAt(0) == '1')
438            max++;
439          else
440            s3 = delete(s3, 0, 1);
441          if (max != s3.length()) {
442            if (max < 0)
443              throw new Error("Unhandled");
444            else if (max < s3.length())
445              s3 = insert(".", s3, max);
446            else
447              throw new Error("Unhandled");
448          }
449
450          Decimal result = new Decimal();
451          try {
452            result.setValueDecimal(s3);
453    } catch (Exception e) {
454        // won't happen
455    }
456          result.negative = neg;
457          result.scientific = scientific || other.scientific;
458          if (decimal < other.decimal)
459                result.precision = precision;
460          else if (other.decimal < decimal)
461                result.precision = other.precision;
462          else
463                result.precision = Math.min(precision, other.precision);
464          return result;
465        }
466
467        private String stringAddition(String s1, String s2) {
468          assert(s1.length() == s2.length());
469          char[] result = new char[s2.length()];
470          for (int i = 0; i < s2.length(); i++)
471                result[i] = '0';
472          int c = 0;
473          for (int i = s1.length() - 1; i >= 0; i--) {
474            int t = c + dig(s1.charAt(i)) + dig(s2.charAt(i));
475            result[i] = cdig(t % 10);
476            c = t / 10;
477          }
478          assert(c == 0);
479          return new String(result);
480        }
481
482        private String stringSubtraction(String s1, String s2) {
483//        i, t, c : integer;
484          assert(s1.length() == s2.length());
485
486          char[] result = new char[s2.length()];
487          for (int i = 0; i < s2.length(); i++)
488                result[i] = '0';
489
490          int c = 0;
491          for (int i = s1.length() - 1; i >= 0; i--) {
492            int t = c + (dig(s1.charAt(i)) - dig(s2.charAt(i)));
493            if (t < 0) {
494              t = t + 10;
495              if (i == 0) 
496                throw new Error("internal logic error");
497              else
498                s1 = replaceChar(s1, i-1, cdig(dig(s1.charAt(i-1))-1));
499            }
500            result[i] = cdig(t);
501          }
502          assert(c == 0);
503          return new String(result);
504        }
505
506        private String replaceChar(String s, int offset, char c) {
507          if (offset == 0)
508                return String.valueOf(c)+s.substring(1);
509          else
510                return s.substring(0, offset)+c+s.substring(offset+1); 
511  }
512
513
514        public Decimal multiply(Decimal other) {
515                if (other == null)
516                        return null;
517
518                if (isZero() || other.isZero())
519                        return zero();
520
521                int max = Math.max(decimal, other.decimal);
522                String s1 = stringMultiply('0', max - decimal+1) + digits;
523                String s2 = stringMultiply('0', max - other.decimal+1) + other.digits;
524                if (s1.length() < s2.length())
525                        s1 = s1 + stringMultiply('0', s2.length() - s1.length());
526                else if (s2.length() < s1.length())
527                        s2 = s2 + stringMultiply('0', s1.length() - s2.length());
528
529                if (s2.compareTo(s1) > 0) {
530                        String s3 = s1;
531                        s1 = s2;
532                        s2 = s3;
533                }
534                String[] s = new String[s2.length()];
535
536                int t = 0;
537                for (int i = s2.length()-1; i >= 0; i--) {
538                        s[i] = stringMultiply('0', s2.length()-(i+1));
539                        int c = 0;
540                        for (int j = s1.length() - 1; j >= 0; j--) {
541                                t = c + (dig(s1.charAt(j)) * dig(s2.charAt(i)));
542                                s[i] = insert(String.valueOf(cdig(t % 10)), s[i], 0);
543                                c = t / 10;
544                        }
545                        while (c > 0) {
546                                s[i] = insert(String.valueOf(cdig(t % 10)), s[i], 0);
547                                c = t / 10;
548                        }
549                }
550
551                t = 0;
552                for (String sv : s)
553                        t = Math.max(t, sv.length());
554                for (int i = 0; i < s.length; i++) 
555                        s[i] = stringMultiply('0', t-s[i].length())+s[i];
556
557                String res = "";
558                int c = 0;
559                for (int i = t - 1; i>= 0; i--) {
560                        for (int j = 0; j < s.length; j++) 
561                                c = c + dig(s[j].charAt(i));
562                        res = insert(String.valueOf(cdig(c %10)), res, 0);
563                        c = c / 10;
564                }
565
566                if (c > 0) {
567                        throw new Error("internal logic error");
568                        //        while..
569                        //        s[i-1] = s[i-1] + cdig(t mod 10);
570                        //        c = t div 10;
571                }
572
573                int dec = res.length() - ((s1.length() - (max+1))*2);
574
575                while (!Utilities.noString(res) && !res.equals("0") && res.startsWith("0")) {
576                        res = res.substring(1);
577                        dec--;
578                }
579
580                int prec = 0;
581                if (isWholeNumber() && other.isWholeNumber())
582                        // at least the specified precision, and possibly more
583                        prec = Math.max(Math.max(digits.length(), other.digits.length()), Math.min(precision, other.precision));
584                else if (isWholeNumber())
585                        prec = other.precision;
586                else if (other.isWholeNumber())
587                        prec = precision;
588                else
589                        prec = Math.min(precision, other.precision);
590                while (res.length() > prec && res.charAt(res.length()-1) == '0')
591                        res = delete(res, res.length()-1, 1);
592
593                Decimal result = new Decimal();
594                try {
595            result.setValueDecimal(res);
596    } catch (Exception e) {
597            // won't happen
598    }
599                result.precision = prec;
600                result.decimal = dec;
601                result.negative = negative != other.negative;
602                result.scientific = scientific || other.scientific;
603                return result;
604        }
605
606        public Decimal divide(Decimal other) throws UcumException  {
607                if (other == null)
608                        return null;
609
610                if (isZero())
611                        return zero();
612
613                if (other.isZero())
614                        throw new UcumException("Attempt to divide "+toString()+" by zero");
615
616                String s = "0"+other.digits;
617                int m = Math.max(digits.length(), other.digits.length()) + 40; // max loops we'll do
618                String[] tens = new String[10];
619                tens[0] = stringAddition(stringMultiply('0', s.length()), s);
620                for (int i = 1; i < 10; i++)
621                        tens[i] = stringAddition(tens[i-1], s);
622                String v = digits;
623                String r = "";
624                int l = 0;
625                int d = (digits.length() - decimal + 1) - (other.digits.length() - other.decimal + 1);
626
627                while (v.length() < tens[0].length()) {
628                        v = v + "0";
629                        d++;
630                }
631
632                String w;
633                int vi;
634                if (v.substring(0, other.digits.length()).compareTo(other.digits) < 0) {
635                        if (v.length() == tens[0].length()) {
636                                v = v + '0';
637                                d++;
638                        }
639                        w = v.substring(0, other.digits.length()+1);
640                        vi = w.length();
641                } else {
642                        w = "0"+v.substring(0, other.digits.length());
643                        vi = w.length()-1;
644                }
645
646                boolean handled = false;
647                boolean proc;
648
649                while (!(handled && ((l > m) || ((vi >= v.length()) && ((Utilities.noString(w) || allZeros(w, 0))))))) {
650                        l++;
651                        handled = true;
652                        proc = false;
653                        for (int i = 8; i >= 0; i--) {
654                                if (tens[i].compareTo(w) <= 0) {
655                                        proc = true;
656                                        r = r + cdig(i+1);
657                                        w = trimLeadingZeros(stringSubtraction(w, tens[i]));
658                                        if (!(handled && ((l > m) || ((vi >= v.length()) && ((Utilities.noString(w) || allZeros(w, 0))))))) {
659                                                if (vi < v.length()) {
660                                                        w = w + v.charAt(vi);
661                                                        vi++;
662                                                        handled = false;
663                                                } else {
664                                                        w = w + '0';
665                                                        d++;
666                                                }
667                                                while (w.length() < tens[0].length()) 
668                                                        w = '0'+w;
669                                        }
670                                        break;
671                                }
672                        }
673                        if (!proc) {
674                                assert(w.charAt(0) == '0');
675                                w = delete(w, 0, 1);
676                                r = r + "0";
677                                if (!(handled && ((l > m) || ((vi >= v.length()) && ((Utilities.noString(w) || allZeros(w, 0))))))) {
678                                        if (vi < v.length()) {
679                                                w = w + v.charAt(vi);
680                                                vi++;
681                                                handled = false;
682                                        } else {
683                                                w = w + '0';
684                                                d++;
685                                        }
686                                        while (w.length() < tens[0].length()) 
687                                                w = '0'+w;
688                                }
689                        }
690                }
691
692                int prec;
693
694                if (isWholeNumber() && other.isWholeNumber() && (l < m)) {
695                        for (int i = 0; i < d; i++) { 
696                                if (r.charAt(r.length()-1) == '0') { 
697                                        r = delete(r, r.length()-1, 1);
698                                        d--;
699                                }
700                        }
701                        prec = 100;
702                } else {
703                        if (isWholeNumber() && other.isWholeNumber())
704                                prec = Math.max(digits.length(), other.digits.length());
705                        else if (isWholeNumber())
706                                prec = Math.max(other.precision, r.length() - d);
707                        else if (other.isWholeNumber())
708                                prec = Math.max(precision, r.length() - d);
709                        else
710                                prec = Math.max(Math.min(precision, other.precision), r.length() - d);
711                        while (r.length() > prec) {
712                                boolean up = (r.charAt(r.length()-1) > '5');
713                                r = delete(r, r.length()-1, 1);
714                                if (up) {
715                                        char[] rs = r.toCharArray();
716                                        int i = r.length()-1;
717                                        while (up && i > 0) {
718                                                up = rs[i] == '9';
719                                                if (up)
720                                                        rs[i] = '0';
721                                                else
722                                                        rs[i] = cdig(dig(rs[i])+1);
723                                                i--;
724                                        }
725                                        if (up) {
726                                                r = '1'+new String(rs);
727                                                d++;
728                                        } else
729                                                r = new String(rs);
730                                }
731                                d--;
732                        }
733                }
734
735                Decimal result = new Decimal();
736                result.setValueDecimal(r);
737                result.decimal = r.length() - d;
738                result.negative = negative != other.negative;
739                result.precision = prec;
740                result.scientific = scientific || other.scientific;
741                return result;
742        }
743
744
745        private String trimLeadingZeros(String s) {
746                if (s == null) 
747                        return null;
748
749                int i = 0;
750                while (i < s.length() && s.charAt(i) == '0') 
751                        i++;
752                if (i == s.length())
753                        return "0";
754                else
755                  return s.substring(i);
756        }
757
758        public Decimal divInt(Decimal other) throws UcumException  {
759          if (other == null)
760                return null;
761          Decimal t = divide(other);
762          return t.trunc();
763        }
764
765        public Decimal modulo(Decimal other) throws UcumException  {
766          if (other == null)  
767      return null;
768          Decimal t = divInt(other);
769          Decimal t2 = t.multiply(other);
770          return subtract(t2);
771        }
772
773        public boolean equals(Decimal value, Decimal maxDifference) {
774          Decimal diff = this.subtract(value).absolute();
775          return diff.comparesTo(maxDifference) <= 0;
776  }
777
778        private Decimal absolute() {
779          Decimal d = copy();
780          d.negative = false;
781          return d;
782  }
783
784
785}