View Javadoc
1   /*
2    * Licensed under the GPL License. You may not use this file except in compliance with the License.
3    * You may obtain a copy of the License at
4    *
5    *   https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
6    *
7    * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
8    * WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
9    * PURPOSE.
10   */
11  package psiprobe.tools;
12  
13  import java.text.NumberFormat;
14  import java.util.regex.Matcher;
15  import java.util.regex.Pattern;
16  
17  /**
18   * Tool for parsing and formatting SI-prefixed numbers in base-2 and base-10.
19   */
20  public final class SizeExpression {
21  
22    /** The Constant MULTIPLIER_2. */
23    public static final long MULTIPLIER_2 = 1024;
24  
25    /** The Constant MULTIPLIER_10. */
26    public static final long MULTIPLIER_10 = 1000;
27  
28    /** The Constant UNIT_BASE. */
29    public static final String UNIT_BASE = "B";
30  
31    /** The Constant PREFIX_KILO. */
32    public static final char PREFIX_KILO = 'K';
33  
34    /** The Constant PREFIX_MEGA. */
35    public static final char PREFIX_MEGA = 'M';
36  
37    /** The Constant PREFIX_GIGA. */
38    public static final char PREFIX_GIGA = 'G';
39  
40    /** The Constant PREFIX_TERA. */
41    public static final char PREFIX_TERA = 'T';
42  
43    /** The Constant PREFIX_PETA. */
44    public static final char PREFIX_PETA = 'P';
45  
46    /**
47     * Prevent Instantiation.
48     */
49    private SizeExpression() {
50      // Prevent Instantiation
51    }
52  
53    /**
54     * Parses the given expression into a numerical value.
55     *
56     * <p>
57     * An expression has three parts:
58     * </p>
59     * <table>
60     * <caption>Size Summary Table</caption>
61     *
62     * <thead>
63     * <tr>
64     * <th>Name</th>
65     * <th>Description</th>
66     * </tr>
67     * </thead>
68     *
69     * <tbody>
70     * <tr>
71     * <td>Base Number</td>
72     * <td>(Required) The mantissa or significand of the expression. This can include decimal
73     * values.</td>
74     * </tr>
75     * <tr>
76     * <td>Prefix</td>
77     * <td>(Optional) The <a href="https://en.wikipedia.org/wiki/Metric_prefix" target="_blank">SI
78     * prefix</a>. These span from K for kilo- to P for peta-.</td>
79     * </tr>
80     * <tr>
81     * <td>Unit</td>
82     * <td>(Optional) If the unit "B" (for bytes) is provided, the prefix is treated as base-2 (1024).
83     * Otherwise, it uses base-10 (1000).</td>
84     * </tr>
85     * </tbody>
86     *
87     * <tfoot>
88     * <tr>
89     * <td colspan="2"><em>Note: Whitespace may or may not exist between the Base Number and
90     * Prefix.</em></td>
91     * </tr>
92     * </tfoot>
93     *
94     * </table>
95     *
96     * <p>
97     * Examples:
98     * </p>
99     * <ul>
100    * <li>"2k" returns {@code 2000}</li>
101    * <li>"3.5m" returns {@code 3500000}</li>
102    * <li>"2kb" returns {@code 2048}</li>
103    * <li>"3.5mb" returns {@code 3670016}</li>
104    * </ul>
105    *
106    * @param expression the expression to parse
107    *
108    * @return the parsed value
109    *
110    * @throws NumberFormatException if the given expression cannot be parsed
111    */
112   public static long parse(String expression) {
113     String prefixClass =
114         "[" + PREFIX_KILO + PREFIX_MEGA + PREFIX_GIGA + PREFIX_TERA + PREFIX_PETA + "]";
115     Pattern expPattern =
116         Pattern.compile("(\\d+|\\d*\\.\\d+)\\s*(" + prefixClass + ")?(" + UNIT_BASE + ")?",
117             Pattern.CASE_INSENSITIVE);
118     Matcher expMatcher = expPattern.matcher(expression);
119     if (expMatcher.matches()) {
120       String value = expMatcher.group(1);
121       String unitPrefix = expMatcher.group(2);
122       String unitBase = expMatcher.group(3);
123       double multiplier = 1;
124       if (unitPrefix != null) {
125         multiplier = multiplier(unitPrefix.charAt(0), unitBase != null);
126       }
127       double rawValue = Double.parseDouble(value);
128       return (long) (rawValue * multiplier);
129     }
130     throw new NumberFormatException("Invalid expression format: " + expression);
131   }
132 
133   /**
134    * Formats the value as an expression.
135    *
136    * @param value the numerical value to be formatted
137    * @param decimalPlaces the number of decimal places in the mantissa
138    * @param base2 whether to use the base-2 (1024) multiplier and format with "B" units. If false,
139    *        uses the base-10 (1000) multiplier and no units.
140    *
141    * @return a formatted string expression of the value
142    */
143   public static String format(long value, int decimalPlaces, boolean base2) {
144     NumberFormat nf = NumberFormat.getInstance();
145     nf.setMinimumFractionDigits(decimalPlaces);
146 
147     double doubleResult;
148     String unit = base2 ? UNIT_BASE : "";
149     double multiplierKilo = multiplier(PREFIX_KILO, base2);
150     double multiplierMega = multiplier(PREFIX_MEGA, base2);
151     double multiplierGiga = multiplier(PREFIX_GIGA, base2);
152     double multiplierTera = multiplier(PREFIX_TERA, base2);
153     double multiplierPeta = multiplier(PREFIX_PETA, base2);
154     if (value < multiplierKilo) {
155       doubleResult = value;
156       nf.setMinimumFractionDigits(0);
157     } else if (value < multiplierMega) {
158       doubleResult = round(value / multiplierKilo, decimalPlaces);
159       unit = PREFIX_KILO + unit;
160     } else if (value < multiplierGiga) {
161       doubleResult = round(value / multiplierMega, decimalPlaces);
162       unit = PREFIX_MEGA + unit;
163     } else if (value < multiplierTera) {
164       doubleResult = round(value / multiplierGiga, decimalPlaces);
165       unit = PREFIX_GIGA + unit;
166     } else if (value < multiplierPeta) {
167       doubleResult = round(value / multiplierTera, decimalPlaces);
168       unit = PREFIX_TERA + unit;
169     } else {
170       doubleResult = round(value / multiplierPeta, decimalPlaces);
171       unit = PREFIX_PETA + unit;
172     }
173     return nf.format(doubleResult) + (base2 ? " " : "") + unit;
174   }
175 
176   /**
177    * Rounds a decimal value to the given decimal place.
178    *
179    * @param value the value to round
180    * @param decimalPlaces the number of decimal places to preserve.
181    *
182    * @return the rounded value
183    */
184   private static double round(double value, int decimalPlaces) {
185     return Math.round(value * Math.pow(10, decimalPlaces)) / Math.pow(10, decimalPlaces);
186   }
187 
188   /**
189    * Returns the base-2 or base-10 multiplier for a given prefix.
190    *
191    * @param unitPrefix the character representing the prefix. Can be K, M, G, T, or P.
192    * @param base2 whether to use the base-2 (1024) multiplier. If false, uses the base-10 (1000)
193    *        multiplier.
194    *
195    * @return the multiplier for the given prefix
196    */
197   private static double multiplier(char unitPrefix, boolean base2) {
198     long result;
199     long multiplier = base2 ? MULTIPLIER_2 : MULTIPLIER_10;
200     switch (Character.toUpperCase(unitPrefix)) {
201       case PREFIX_KILO:
202         result = multiplier;
203         break;
204       case PREFIX_MEGA:
205         result = multiplier * multiplier;
206         break;
207       case PREFIX_GIGA:
208         result = multiplier * multiplier * multiplier;
209         break;
210       case PREFIX_TERA:
211         result = multiplier * multiplier * multiplier * multiplier;
212         break;
213       case PREFIX_PETA:
214         result = multiplier * multiplier * multiplier * multiplier * multiplier;
215         break;
216       default:
217         throw new IllegalArgumentException("Invalid unit prefix: " + unitPrefix);
218     }
219     return result;
220   }
221 
222 }