summaryrefslogtreecommitdiff
path: root/src/main/java/sevenUnits/unit/LinearUnit.java
blob: 71911968bb8ef2fca51cd7075200fc084f988445 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
/**
 * Copyright (C) 2019, 2021, 2022, 2024, 2025 Adrien Hopkins
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package sevenUnits.unit;

import java.util.Objects;

import sevenUnits.utils.DecimalComparison;
import sevenUnits.utils.NameSymbol;
import sevenUnits.utils.ObjectProduct;
import sevenUnits.utils.UncertainDouble;

/**
 * A unit that can be expressed as a product of its base and a number. For
 * example, kilometres, inches and pounds.
 * 
 * @author Adrien Hopkins
 * @since 2019-10-16
 * @since v0.3.0
 */
public final class LinearUnit extends Unit {
	/**
	 * Gets a {@code LinearUnit} from a unit and a value. For example, converts
	 * '59 °F' to a linear unit with the value of '288.15 K'
	 * 
	 * @param unit  unit to convert
	 * @param value value to convert
	 * @return value expressed as a {@code LinearUnit}
	 * @since 2019-10-16
	 * @since v0.3.0
	 * @throws NullPointerException if unit is null
	 */
	public static LinearUnit fromUnitValue(final Unit unit, final double value) {
		return new LinearUnit(
				Objects.requireNonNull(unit, "unit must not be null.").getBase(),
				unit.convertToBase(value), NameSymbol.EMPTY);
	}
	
	/**
	 * Gets a {@code LinearUnit} from a unit and a value. For example, converts
	 * '59 °F' to a linear unit with the value of '288.15 K'
	 * 
	 * @param unit  unit to convert
	 * @param value value to convert
	 * @param ns    name(s) and symbol of unit
	 * @return value expressed as a {@code LinearUnit}
	 * @since 2019-10-21
	 * @since v0.3.0
	 * @throws NullPointerException if unit or ns is null
	 */
	public static LinearUnit fromUnitValue(final Unit unit, final double value,
			final NameSymbol ns) {
		return new LinearUnit(
				Objects.requireNonNull(unit, "unit must not be null.").getBase(),
				unit.convertToBase(value), ns);
	}
	
	/**
	 * @param unit unit to get base version of
	 * @return the base unit associated with {@code unit}, as a
	 *         {@code LinearUnit}.
	 * @since 2020-10-02
	 * @since v0.3.0
	 */
	public static LinearUnit getBase(final Unit unit) {
		return new LinearUnit(unit.getBase(), 1, NameSymbol.EMPTY);
	}
	
	/**
	 * Gets a {@code LinearUnit} from a unit base and a conversion factor. In
	 * other words, gets the product of {@code unitBase} and
	 * {@code conversionFactor}, expressed as a {@code LinearUnit}.
	 * 
	 * @param unitBase         unit base to multiply by
	 * @param conversionFactor number to multiply base by
	 * @return product of base and conversion factor
	 * @since 2019-10-16
	 * @since v0.3.0
	 * @throws NullPointerException if unitBase is null
	 */
	public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase,
			final double conversionFactor) {
		return new LinearUnit(unitBase, conversionFactor, NameSymbol.EMPTY);
	}
	
	/**
	 * Gets a {@code LinearUnit} from a unit base and a conversion factor. In
	 * other words, gets the product of {@code unitBase} and
	 * {@code conversionFactor}, expressed as a {@code LinearUnit}.
	 * 
	 * @param unitBase         unit base to multiply by
	 * @param conversionFactor number to multiply base by
	 * @param ns               name(s) and symbol of unit
	 * @return product of base and conversion factor
	 * @since 2019-10-21
	 * @since v0.3.0
	 * @throws NullPointerException if unitBase is null
	 */
	public static LinearUnit valueOf(final ObjectProduct<BaseUnit> unitBase,
			final double conversionFactor, final NameSymbol ns) {
		return new LinearUnit(unitBase, conversionFactor, ns);
	}
	
	/**
	 * The value of this unit as represented in its base form. Mathematically,
	 * 
	 * <pre>
	 * this = conversionFactor * getBase()
	 * </pre>
	 * 
	 * @since 2019-10-16
	 * @since v0.3.0
	 */
	private final double conversionFactor;
	
	/**
	 * Creates the {@code LinearUnit}.
	 * 
	 * @param unitBase         base of linear unit
	 * @param conversionFactor conversion factor between base and unit
	 * @since 2019-10-16
	 * @since v0.3.0
	 */
	private LinearUnit(final ObjectProduct<BaseUnit> unitBase,
			final double conversionFactor, final NameSymbol ns) {
		super(unitBase, ns);
		this.conversionFactor = conversionFactor;
	}
	
	/**
	 * {@inheritDoc}
	 * 
	 * Converts by dividing by {@code conversionFactor}
	 */
	@Override
	protected double convertFromBase(final double value) {
		return value / this.getConversionFactor();
	}
	
	/**
	 * Converts an {@code UncertainDouble} value expressed in this unit to an
	 * {@code UncertainValue} value expressed in {@code other}.
	 * 
	 * @param other unit to convert to
	 * @param value value to convert
	 * @return converted value
	 * @since 2019-09-07
	 * @since v0.3.0
	 * @throws IllegalArgumentException if {@code other} is incompatible for
	 *                                  conversion with this unit (as tested by
	 *                                  {@link Unit#canConvertTo}).
	 * @throws NullPointerException     if value or other is null
	 */
	public UncertainDouble convertTo(LinearUnit other, UncertainDouble value) {
		Objects.requireNonNull(other, "other must not be null.");
		Objects.requireNonNull(value, "value may not be null.");
		if (this.canConvertTo(other))
			return value.timesExact(
					this.getConversionFactor() / other.getConversionFactor());
		else
			throw new IllegalArgumentException(
					String.format("Cannot convert from %s to %s.", this, other));
		
	}
	
	/**
	 * {@inheritDoc}
	 * 
	 * Converts by multiplying by {@code conversionFactor}
	 */
	@Override
	protected double convertToBase(final double value) {
		return value * this.getConversionFactor();
	}
	
	/**
	 * Converts an {@code UncertainDouble} to the base unit.
	 * 
	 * @since 2020-09-07
	 * @since v0.3.0
	 */
	UncertainDouble convertToBase(final UncertainDouble value) {
		return value.timesExact(this.getConversionFactor());
	}
	
	/**
	 * Divides this unit by a scalar.
	 * 
	 * @param divisor scalar to divide by
	 * @return quotient
	 * @since 2018-12-23
	 * @since v0.1.0
	 */
	public LinearUnit dividedBy(final double divisor) {
		return valueOf(this.getBase(), this.getConversionFactor() / divisor);
	}
	
	/**
	 * Returns the quotient of this unit and another.
	 * 
	 * @param divisor unit to divide by
	 * @return quotient of two units
	 * @throws NullPointerException if {@code divisor} is null
	 * @since 2018-12-22
	 * @since v0.1.0
	 */
	public LinearUnit dividedBy(final LinearUnit divisor) {
		Objects.requireNonNull(divisor, "other must not be null");
		
		// divide the units
		final ObjectProduct<BaseUnit> base = this.getBase()
				.dividedBy(divisor.getBase());
		return valueOf(base,
				this.getConversionFactor() / divisor.getConversionFactor());
	}
	
	/**
	 * {@inheritDoc}
	 * 
	 * Uses the base and conversion factor of units to test for equality.
	 */
	@Override
	public boolean equals(final Object obj) {
		if (!(obj instanceof LinearUnit))
			return false;
		final LinearUnit other = (LinearUnit) obj;
		return Objects.equals(this.getBase(), other.getBase())
				&& Double.compare(this.getConversionFactor(),
						other.getConversionFactor()) == 0;
	}

	/**
	 * @param other unit to test equality with
	 * @return true iff this unit and other are equal,
	 * ignoring small differences caused by floating-point error.
	 * 
	 * @apiNote This method is not transitive,
	 * so it cannot be used as an equals method.
	 */
	public boolean equalsApproximately(final LinearUnit other) {
		return Objects.equals(this.getBase(), other.getBase())
				&& DecimalComparison.equals(this.getConversionFactor(),
						other.getConversionFactor());
	}
	
	/**
	 * @return conversion factor
	 * @since 2019-10-16
	 * @since v0.3.0
	 */
	public double getConversionFactor() {
		return this.conversionFactor;
	}
	
	/**
	 * {@inheritDoc}
	 * 
	 * Uses the base and conversion factor to compute a hash code.
	 */
	@Override
	public int hashCode() {
		return 31 * this.getBase().hashCode()
				+ Double.hashCode(this.getConversionFactor());
	}
	
	/**
	 * @return whether this unit is equivalent to a {@code BaseUnit} (i.e. there
	 *         is a {@code BaseUnit b} where
	 *         {@code b.asLinearUnit().equals(this)} returns {@code true}.)
	 * @since 2019-10-16
	 * @since v0.3.0
	 */
	public boolean isBase() {
		return this.isCoherent() && this.getBase().isSingleObject();
	}
	
	/**
	 * @return whether this unit is coherent (i.e. has conversion factor 1)
	 * @since 2019-10-16
	 * @since v0.3.0
	 */
	public boolean isCoherent() {
		return this.getConversionFactor() == 1;
	}
	
	/**
	 * Returns the difference of this unit and another.
	 * <p>
	 * Two units can be subtracted if they have the same base. Note that
	 * {@link #canConvertTo} can be used to determine this. If {@code subtrahend}
	 * does not meet this condition, an {@code IllegalArgumentException} will be
	 * thrown.
	 * </p>
	 * 
	 * @param subtrahend unit to subtract
	 * @return difference of units
	 * @throws IllegalArgumentException if {@code subtrahend} is not compatible
	 *                                  for subtraction as described above
	 * @throws NullPointerException     if {@code subtrahend} is null
	 * @since 2019-03-17
	 * @since v0.2.0
	 */
	public LinearUnit minus(final LinearUnit subtrahend) {
		Objects.requireNonNull(subtrahend, "addend must not be null.");
		
		// reject subtrahends that cannot be added to this unit
		if (!this.getBase().equals(subtrahend.getBase()))
			throw new IllegalArgumentException(String.format(
					"Incompatible units for subtraction \"%s\" and \"%s\".", this,
					subtrahend));
		
		// subtract the units
		return valueOf(this.getBase(),
				this.getConversionFactor() - subtrahend.getConversionFactor());
	}
	
	/**
	 * Returns the sum of this unit and another.
	 * <p>
	 * Two units can be added if they have the same base. Note that
	 * {@link #canConvertTo} can be used to determine this. If {@code addend}
	 * does not meet this condition, an {@code IllegalArgumentException} will be
	 * thrown.
	 * </p>
	 * 
	 * @param addend unit to add
	 * @return sum of units
	 * @throws IllegalArgumentException if {@code addend} is not compatible for
	 *                                  addition as described above
	 * @throws NullPointerException     if {@code addend} is null
	 * @since 2019-03-17
	 * @since v0.2.0
	 */
	public LinearUnit plus(final LinearUnit addend) {
		Objects.requireNonNull(addend, "addend must not be null.");
		
		// reject addends that cannot be added to this unit
		if (!this.getBase().equals(addend.getBase()))
			throw new IllegalArgumentException(String.format(
					"Incompatible units for addition \"%s\" and \"%s\".", this,
					addend));
		
		// add the units
		return valueOf(this.getBase(),
				this.getConversionFactor() + addend.getConversionFactor());
	}
	
	/**
	 * Multiplies this unit by a scalar.
	 * 
	 * @param multiplier scalar to multiply by
	 * @return product
	 * @since 2018-12-23
	 * @since v0.1.0
	 */
	public LinearUnit times(final double multiplier) {
		return valueOf(this.getBase(), this.getConversionFactor() * multiplier);
	}
	
	/**
	 * Returns the product of this unit and another.
	 * 
	 * @param multiplier unit to multiply by
	 * @return product of two units
	 * @throws NullPointerException if {@code multiplier} is null
	 * @since 2018-12-22
	 * @since v0.1.0
	 */
	public LinearUnit times(final LinearUnit multiplier) {
		Objects.requireNonNull(multiplier, "other must not be null");
		
		// multiply the units
		final ObjectProduct<BaseUnit> base = this.getBase()
				.times(multiplier.getBase());
		return valueOf(base,
				this.getConversionFactor() * multiplier.getConversionFactor());
	}
	
	@Override
	public String toDefinitionString() {
		return Double.toString(this.conversionFactor)
				+ (this.getBase().equals(ObjectProduct.empty()) ? ""
						: " " + this.getBase().toString(BaseUnit::getShortName));
	}
	
	/**
	 * Returns this unit but to an exponent.
	 * 
	 * @param exponent exponent to exponentiate unit to
	 * @return exponentiated unit
	 * @since 2019-01-15
	 * @since v0.1.0
	 */
	public LinearUnit toExponent(final int exponent) {
		return valueOf(this.getBase().toExponent(exponent),
				Math.pow(this.conversionFactor, exponent));
	}
	
	/**
	 * Returns this unit to an exponent, rounding the resulting dimensions to the
	 * nearest integer.
	 * 
	 * @param exponent exponent to raise unit to
	 * @return result of rounded exponentation
	 * @since 2024-08-22
	 * @since v1.0.0
	 * @see ObjectProduct#toExponentRounded
	 */
	public LinearUnit toExponentRounded(final double exponent) {
		return valueOf(this.getBase().toExponentRounded(exponent),
				Math.pow(this.conversionFactor, exponent));
	}
	
	@Override
	public LinearUnit withName(final NameSymbol ns) {
		return valueOf(this.getBase(), this.getConversionFactor(), ns);
	}
	
	/**
	 * Returns the result of applying {@code prefix} to this unit.
	 * <p>
	 * If this unit and the provided prefix have a primary name, the returned
	 * unit will have a primary name (prefix's name + unit's name). <br>
	 * If this unit and the provided prefix have a symbol, the returned unit will
	 * have a symbol. <br>
	 * This method ignores alternate names of both this unit and the provided
	 * prefix.
	 * 
	 * @param prefix prefix to apply
	 * @return unit with prefix
	 * @since 2019-03-18
	 * @since v0.2.0
	 * @throws NullPointerException if prefix is null
	 */
	public LinearUnit withPrefix(final UnitPrefix prefix) {
		final LinearUnit unit = this.times(prefix.getMultiplier());
		
		// create new name and symbol, if possible
		final String name;
		if (this.getPrimaryName().isPresent()
				&& prefix.getPrimaryName().isPresent()) {
			name = prefix.getPrimaryName().get() + this.getPrimaryName().get();
		} else {
			name = null;
		}
		
		final String symbol;
		if (this.getSymbol().isPresent() && prefix.getSymbol().isPresent()) {
			symbol = prefix.getSymbol().get() + this.getSymbol().get();
		} else {
			symbol = null;
		}
		
		return unit.withName(NameSymbol.ofNullable(name, symbol));
	}
}