| 1 | package net.mattmccutchen.measurements; |
| 2 | |
| 3 | import java.text.*; |
| 4 | |
| 5 | public class Measurement { |
| 6 | public static final int dataLen = 2 * 8 + Unit.basicUnits.length * 1; |
| 7 | public static final String errorIndicator = "ERROR"; |
| 8 | public static final char exactIndicator = 'x'; |
| 9 | public static final int[] pureNumberUnitPowers = new int[Unit.basicUnits.length]; |
| 10 | |
| 11 | public final double number; |
| 12 | public final double uncertainty; |
| 13 | public final int[] unitPowers; // Don't mutate |
| 14 | |
| 15 | public Measurement(double number, double uncertainty, int... unitPowers) { |
| 16 | this.number = number; |
| 17 | this.uncertainty = uncertainty; |
| 18 | this.unitPowers = unitPowers; |
| 19 | } |
| 20 | |
| 21 | public static Measurement parse(String str) { |
| 22 | try { |
| 23 | int space = str.indexOf(' '); |
| 24 | String numPart; |
| 25 | String unitPart; |
| 26 | if (space == -1) { |
| 27 | numPart = str; |
| 28 | unitPart = ""; |
| 29 | } else { |
| 30 | numPart = str.substring(0, space); |
| 31 | unitPart = str.substring(space + 1); |
| 32 | } |
| 33 | double num; |
| 34 | double unc; |
| 35 | if (numPart.charAt(numPart.length() - 1) == exactIndicator) { |
| 36 | numPart = numPart.substring(0, numPart.length() - 1); |
| 37 | unc = 0; |
| 38 | num = Double.parseDouble(numPart); |
| 39 | } else { |
| 40 | num = Double.parseDouble(numPart); |
| 41 | // Determine sig figs |
| 42 | int echar = numPart.indexOf('e'); |
| 43 | if (echar == -1) { |
| 44 | echar = numPart.indexOf('E'); |
| 45 | if (echar == -1) |
| 46 | echar = numPart.length(); |
| 47 | } |
| 48 | int decpt = numPart.indexOf('.'); |
| 49 | int sigFigs; |
| 50 | if (decpt == -1) { |
| 51 | // Everything but trailing zeros. Yuck. |
| 52 | int i = echar; |
| 53 | while (i > 1 /* 0 gets one sig fig */ |
| 54 | && numPart.charAt(i - 1) == '0') |
| 55 | i--; |
| 56 | sigFigs = i; |
| 57 | } else |
| 58 | sigFigs = echar - 1; // Everything except the decimal point |
| 59 | unc = Math.pow(10.0, expOf(num) - sigFigs); |
| 60 | } |
| 61 | |
| 62 | Unit theUnit = Unit.parseUnitString(unitPart); |
| 63 | num *= theUnit.factor; |
| 64 | unc *= theUnit.factor; |
| 65 | return new Measurement(num, unc, theUnit.powers); |
| 66 | } catch (Exception e) { |
| 67 | return null; |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | public static Measurement parseCode(String str) { |
| 72 | if (str.indexOf(errorIndicator) != -1) |
| 73 | return null; |
| 74 | // Does it have a code? |
| 75 | int lbrack = str.indexOf('['); |
| 76 | if (lbrack != -1) { |
| 77 | int rbrack = str.indexOf(']', lbrack + 1); |
| 78 | String code = str.substring(lbrack + 1, rbrack); |
| 79 | byte[] data = Base64.decode(code, Base64.NO_GZIP_MAGIC); |
| 80 | return fromData(data); |
| 81 | } else |
| 82 | return parse(str); |
| 83 | } |
| 84 | |
| 85 | public static Measurement fromData(byte[] data) { |
| 86 | /* |
| 87 | System.err.print("Loading data: "); |
| 88 | for (int i = 0; i < data.length; i++) |
| 89 | System.err.print(" " + data[i]); |
| 90 | System.err.println(); |
| 91 | */ |
| 92 | double num = BitFlicking.loadDouble(data, 0); |
| 93 | double unc = BitFlicking.loadDouble(data, 8); |
| 94 | int[] up = new int[Unit.basicUnits.length]; |
| 95 | for (int i = 0; i < up.length; i++) |
| 96 | up[i] = data[16+i]; |
| 97 | return new Measurement(num, unc, up); |
| 98 | } |
| 99 | |
| 100 | public byte[] toData() { |
| 101 | byte[] data = new byte[dataLen]; |
| 102 | BitFlicking.storeDouble(data, number, 0); |
| 103 | BitFlicking.storeDouble(data, uncertainty, 8); |
| 104 | for (int i = 0; i < unitPowers.length; i++) |
| 105 | data[16+i] = (byte) unitPowers[i]; |
| 106 | /* |
| 107 | System.err.print("Storing data: "); |
| 108 | for (int i = 0; i < data.length; i++) |
| 109 | System.err.print(" " + data[i]); |
| 110 | System.err.println(); |
| 111 | */ |
| 112 | return data; |
| 113 | } |
| 114 | |
| 115 | private static double expOf(double d) { |
| 116 | return (d == 0) ? 0 : Math.floor(Math.log10(d)); |
| 117 | } |
| 118 | |
| 119 | private static int sigFigsOf(double num, double unc) { |
| 120 | if (unc == 0) |
| 121 | return Integer.MAX_VALUE; |
| 122 | //System.err.println("number " + number + ", unc " + uncertainty); |
| 123 | int sf = (int) Math.round(expOf(num) - Math.log10(unc)); |
| 124 | //System.err.println("sigFigs: " + sf); |
| 125 | return sf; |
| 126 | } |
| 127 | |
| 128 | private static DecimalFormat formatForSigFigs(int sigFigs) { |
| 129 | StringBuilder fmtString = new StringBuilder("0."); |
| 130 | for (int i = 0; i < sigFigs-1; i++) |
| 131 | fmtString.append('0'); |
| 132 | fmtString.append("E0"); |
| 133 | //System.err.println("fmtString is " + fmtString); |
| 134 | return new DecimalFormat(fmtString.toString()); |
| 135 | } |
| 136 | |
| 137 | private static final DecimalFormat exactFormat |
| 138 | = new DecimalFormat("0.##########E0"); |
| 139 | |
| 140 | private static void formatNum(double num, double unc, StringBuilder sb) { |
| 141 | int sigFigs = sigFigsOf(num, unc); |
| 142 | if (num == 0 && unc == 0) |
| 143 | sb.append("0" + exactIndicator); |
| 144 | else if (sigFigs > 10 && num != 0) { |
| 145 | // Put some reasonable number of digits and mark exact |
| 146 | sb.append(exactFormat.format(num)); |
| 147 | sb.append(exactIndicator); |
| 148 | } else if (num == 0) { |
| 149 | // Handle zero specially |
| 150 | sb.append("0.E"); |
| 151 | sb.append(-(sigFigs - 1)); |
| 152 | } else if (sigFigs >= 1) |
| 153 | sb.append(formatForSigFigs(sigFigs).format(num)); |
| 154 | else |
| 155 | sb.append("insignificant"); |
| 156 | } |
| 157 | |
| 158 | public static String format(Measurement m, boolean withCode) { |
| 159 | if (m == null) |
| 160 | return errorIndicator; |
| 161 | StringBuilder sb = new StringBuilder(); |
| 162 | formatNum(m.number, m.uncertainty, sb); |
| 163 | |
| 164 | // Units |
| 165 | for (int i = 0; i < m.unitPowers.length; i++) |
| 166 | if (m.unitPowers[i] != 0) { |
| 167 | sb.append(' '); |
| 168 | sb.append(Unit.basicUnits[i].symbol); |
| 169 | if (m.unitPowers[i] != 1) { |
| 170 | sb.append('^'); |
| 171 | sb.append(Integer.toString(m.unitPowers[i])); |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | if (withCode) { |
| 176 | sb.append(" ["); |
| 177 | sb.append(Base64.encodeBytes(m.toData(), Base64.DONT_BREAK_LINES)); |
| 178 | sb.append(']'); |
| 179 | } |
| 180 | return sb.toString(); |
| 181 | } |
| 182 | |
| 183 | public static String formatInUnit(Measurement m, Unit u, boolean withCode) { |
| 184 | if (m == null) |
| 185 | return errorIndicator; |
| 186 | for (int i = 0; i < Unit.basicUnits.length; i++) |
| 187 | if (m.unitPowers[i] != u.powers[i]) |
| 188 | return errorIndicator; |
| 189 | StringBuilder sb = new StringBuilder(); |
| 190 | formatNum(m.number / u.factor, m.uncertainty / u.factor, sb); |
| 191 | sb.append(' '); |
| 192 | sb.append(u.symbol); |
| 193 | |
| 194 | if (withCode) { |
| 195 | sb.append(" ["); |
| 196 | sb.append(Base64.encodeBytes(m.toData(), Base64.DONT_BREAK_LINES)); |
| 197 | sb.append(']'); |
| 198 | } |
| 199 | return sb.toString(); |
| 200 | } |
| 201 | } |