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