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}