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}