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