001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.util.unit;
018
019import java.io.Serializable;
020import java.util.regex.Matcher;
021import java.util.regex.Pattern;
022
023import org.springframework.lang.Nullable;
024import org.springframework.util.Assert;
025import org.springframework.util.StringUtils;
026
027/**
028 * A data size, such as '12MB'.
029 *
030 * <p>This class models data size in terms of bytes and is immutable and thread-safe.
031 *
032 * <p>The terms and units used in this class are based on
033 * <a href="https://en.wikipedia.org/wiki/Binary_prefix">binary prefixes</a>
034 * indicating multiplication by powers of 2. Consult the following table and
035 * the Javadoc for {@link DataUnit} for details.
036 *
037 * <p>
038 * <table border="1">
039 * <tr><th>Term</th><th>Data Size</th><th>Size in Bytes</th></tr>
040 * <tr><td>byte</td><td>1B</td><td>1</td></tr>
041 * <tr><td>kilobyte</td><td>1KB</td><td>1,024</td></tr>
042 * <tr><td>megabyte</td><td>1MB</td><td>1,048,576</td></tr>
043 * <tr><td>gigabyte</td><td>1GB</td><td>1,073,741,824</td></tr>
044 * <tr><td>terabyte</td><td>1TB</td><td>1,099,511,627,776</td></tr>
045 * </table>
046 *
047 * @author Stephane Nicoll
048 * @author Sam Brannen
049 * @since 5.1
050 * @see DataUnit
051 */
052@SuppressWarnings("serial")
053public final class DataSize implements Comparable<DataSize>, Serializable {
054
055        /**
056         * The pattern for parsing.
057         */
058        private static final Pattern PATTERN = Pattern.compile("^([+\\-]?\\d+)([a-zA-Z]{0,2})$");
059
060        /**
061         * Bytes per Kilobyte.
062         */
063        private static final long BYTES_PER_KB = 1024;
064
065        /**
066         * Bytes per Megabyte.
067         */
068        private static final long BYTES_PER_MB = BYTES_PER_KB * 1024;
069
070        /**
071         * Bytes per Gigabyte.
072         */
073        private static final long BYTES_PER_GB = BYTES_PER_MB * 1024;
074
075        /**
076         * Bytes per Terabyte.
077         */
078        private static final long BYTES_PER_TB = BYTES_PER_GB * 1024;
079
080
081        private final long bytes;
082
083
084        private DataSize(long bytes) {
085                this.bytes = bytes;
086        }
087
088
089        /**
090         * Obtain a {@link DataSize} representing the specified number of bytes.
091         * @param bytes the number of bytes, positive or negative
092         * @return a {@link DataSize}
093         */
094        public static DataSize ofBytes(long bytes) {
095                return new DataSize(bytes);
096        }
097
098        /**
099         * Obtain a {@link DataSize} representing the specified number of kilobytes.
100         * @param kilobytes the number of kilobytes, positive or negative
101         * @return a {@link DataSize}
102         */
103        public static DataSize ofKilobytes(long kilobytes) {
104                return new DataSize(Math.multiplyExact(kilobytes, BYTES_PER_KB));
105        }
106
107        /**
108         * Obtain a {@link DataSize} representing the specified number of megabytes.
109         * @param megabytes the number of megabytes, positive or negative
110         * @return a {@link DataSize}
111         */
112        public static DataSize ofMegabytes(long megabytes) {
113                return new DataSize(Math.multiplyExact(megabytes, BYTES_PER_MB));
114        }
115
116        /**
117         * Obtain a {@link DataSize} representing the specified number of gigabytes.
118         * @param gigabytes the number of gigabytes, positive or negative
119         * @return a {@link DataSize}
120         */
121        public static DataSize ofGigabytes(long gigabytes) {
122                return new DataSize(Math.multiplyExact(gigabytes, BYTES_PER_GB));
123        }
124
125        /**
126         * Obtain a {@link DataSize} representing the specified number of terabytes.
127         * @param terabytes the number of terabytes, positive or negative
128         * @return a {@link DataSize}
129         */
130        public static DataSize ofTerabytes(long terabytes) {
131                return new DataSize(Math.multiplyExact(terabytes, BYTES_PER_TB));
132        }
133
134        /**
135         * Obtain a {@link DataSize} representing an amount in the specified {@link DataUnit}.
136         * @param amount the amount of the size, measured in terms of the unit,
137         * positive or negative
138         * @return a corresponding {@link DataSize}
139         */
140        public static DataSize of(long amount, DataUnit unit) {
141                Assert.notNull(unit, "Unit must not be null");
142                return new DataSize(Math.multiplyExact(amount, unit.size().toBytes()));
143        }
144
145        /**
146         * Obtain a {@link DataSize} from a text string such as {@code 12MB} using
147         * {@link DataUnit#BYTES} if no unit is specified.
148         * <p>
149         * Examples:
150         * <pre>
151         * "12KB" -- parses as "12 kilobytes"
152         * "5MB"  -- parses as "5 megabytes"
153         * "20"   -- parses as "20 bytes"
154         * </pre>
155         * @param text the text to parse
156         * @return the parsed {@link DataSize}
157         * @see #parse(CharSequence, DataUnit)
158         */
159        public static DataSize parse(CharSequence text) {
160                return parse(text, null);
161        }
162
163        /**
164         * Obtain a {@link DataSize} from a text string such as {@code 12MB} using
165         * the specified default {@link DataUnit} if no unit is specified.
166         * <p>
167         * The string starts with a number followed optionally by a unit matching one of the
168         * supported {@linkplain DataUnit suffixes}.
169         * <p>
170         * Examples:
171         * <pre>
172         * "12KB" -- parses as "12 kilobytes"
173         * "5MB"  -- parses as "5 megabytes"
174         * "20"   -- parses as "20 kilobytes" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})
175         * </pre>
176         * @param text the text to parse
177         * @return the parsed {@link DataSize}
178         */
179        public static DataSize parse(CharSequence text, @Nullable DataUnit defaultUnit) {
180                Assert.notNull(text, "Text must not be null");
181                try {
182                        Matcher matcher = PATTERN.matcher(text);
183                        Assert.state(matcher.matches(), "Does not match data size pattern");
184                        DataUnit unit = determineDataUnit(matcher.group(2), defaultUnit);
185                        long amount = Long.parseLong(matcher.group(1));
186                        return DataSize.of(amount, unit);
187                }
188                catch (Exception ex) {
189                        throw new IllegalArgumentException("'" + text + "' is not a valid data size", ex);
190                }
191        }
192
193        private static DataUnit determineDataUnit(String suffix, @Nullable DataUnit defaultUnit) {
194                DataUnit defaultUnitToUse = (defaultUnit != null ? defaultUnit : DataUnit.BYTES);
195                return (StringUtils.hasLength(suffix) ? DataUnit.fromSuffix(suffix) : defaultUnitToUse);
196        }
197
198        /**
199         * Checks if this size is negative, excluding zero.
200         * @return true if this size has a size less than zero bytes
201         */
202        public boolean isNegative() {
203                return this.bytes < 0;
204        }
205
206        /**
207         * Return the number of bytes in this instance.
208         * @return the number of bytes
209         */
210        public long toBytes() {
211                return this.bytes;
212        }
213
214        /**
215         * Return the number of kilobytes in this instance.
216         * @return the number of kilobytes
217         */
218        public long toKilobytes() {
219                return this.bytes / BYTES_PER_KB;
220        }
221
222        /**
223         * Return the number of megabytes in this instance.
224         * @return the number of megabytes
225         */
226        public long toMegabytes() {
227                return this.bytes / BYTES_PER_MB;
228        }
229
230        /**
231         * Return the number of gigabytes in this instance.
232         * @return the number of gigabytes
233         */
234        public long toGigabytes() {
235                return this.bytes / BYTES_PER_GB;
236        }
237
238        /**
239         * Return the number of terabytes in this instance.
240         * @return the number of terabytes
241         */
242        public long toTerabytes() {
243                return this.bytes / BYTES_PER_TB;
244        }
245
246        @Override
247        public int compareTo(DataSize other) {
248                return Long.compare(this.bytes, other.bytes);
249        }
250
251        @Override
252        public String toString() {
253                return String.format("%dB", this.bytes);
254        }
255
256
257        @Override
258        public boolean equals(@Nullable Object other) {
259                if (this == other) {
260                        return true;
261                }
262                if (other == null || getClass() != other.getClass()) {
263                        return false;
264                }
265                DataSize otherSize = (DataSize) other;
266                return (this.bytes == otherSize.bytes);
267        }
268
269        @Override
270        public int hashCode() {
271                return Long.hashCode(this.bytes);
272        }
273
274}