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