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