Skip to content

Commit

Permalink
Introduce Count
Browse files Browse the repository at this point in the history
  • Loading branch information
kokosing committed Feb 5, 2019
1 parent 7622bb8 commit 9f22896
Show file tree
Hide file tree
Showing 9 changed files with 986 additions and 0 deletions.
205 changes: 205 additions & 0 deletions src/main/java/io/airlift/units/Count.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.airlift.units;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static io.airlift.units.Preconditions.checkArgument;
import static java.lang.Long.parseLong;
import static java.lang.Math.floor;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

public class Count
implements Comparable<Count>
{
private static final Pattern PATTERN = Pattern.compile("^\\s*(\\d+)\\s*([a-zA-Z]?)\\s*$");

// We iterate over the MAGNITUDES constant in convertToMostSuccinctRounded()
// instead of Magnitude.values() as the latter results in non-trivial amount of memory
// allocation when that method is called in a tight loop. The reason is that the values()
// call allocates a new array at each call.
private static final Magnitude[] MAGNITUDES = Magnitude.values();

/**
* @return count with no bigger value than 1000 in succinct unit, fractional part is rounded
*/
public static Count succinctRounded(long count)
{
return succinctRounded(count, Magnitude.SINGLE);
}

/**
* @return count with no bigger value than 1000 in succinct unit, fractional part is rounded
*/
public static Count succinctRounded(long count, Magnitude magnitude)
{
return new Count(count, magnitude).convertToMostSuccinctRounded();
}

private final long value;
private final Magnitude magnitude;

public Count(long count, Magnitude magnitude)
{
checkArgument(!Double.isInfinite(count), "count is infinite");
checkArgument(!Double.isNaN(count), "count is not a number");
checkArgument(count >= 0, "count is negative");
requireNonNull(magnitude, "unit is null");

this.value = count;
this.magnitude = magnitude;
}

public long getValue()
{
return value;
}

public Magnitude getMagnitude()
{
return magnitude;
}

public long getValue(Magnitude magnitude)
{
if (value == 0L) {
return 0L;
}

long scale = this.magnitude.getFactor() / magnitude.getFactor();
if (scale == 0) {
throw new IllegalArgumentException(format("Unable to return value %s in unit %s due precision loss", this, magnitude));
}
return value * scale;
}

public Count convertTo(Magnitude magnitude)
{
requireNonNull(magnitude, "unit is null");
return new Count(getValue(magnitude), magnitude);
}

/**
* @return converted count with no bigger value than 1000 in succinct unit, fractional part is rounded
*/
public Count convertToMostSuccinctRounded()
{
for (Magnitude magnitude : MAGNITUDES) {
double converted = (double) value * this.magnitude.getFactor() / magnitude.getFactor();
if (converted < 1000) {
return new Count(Math.round(converted), magnitude);
}
}
throw new IllegalStateException();
}

@JsonValue
@Override
public String toString()
{
//noinspection FloatingPointEquality
if (floor(value) == value) {
return (long) (floor(value)) + magnitude.getUnitString();
}

return format(Locale.ENGLISH, "%.2f%s", value, magnitude.getUnitString());
}

@JsonCreator
public static Count valueOf(String count)
throws IllegalArgumentException
{
requireNonNull(count, "count is null");
checkArgument(!count.isEmpty(), "count is empty");

Matcher matcher = PATTERN.matcher(count);
if (!matcher.matches()) {
throw new IllegalArgumentException("Not a valid count string: " + count);
}

long value = parseLong(matcher.group(1));
String unitString = matcher.group(2);

for (Magnitude magnitude : Magnitude.values()) {
if (magnitude.getUnitString().equals(unitString)) {
return new Count(value, magnitude);
}
}

throw new IllegalArgumentException("Unknown unit: " + unitString);
}

@Override
public int compareTo(Count o)
{
return Double.compare(getValue(Magnitude.SINGLE), o.getValue(Magnitude.SINGLE));
}

@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}

Count count = (Count) o;

return compareTo(count) == 0;
}

@Override
public int hashCode()
{
return Double.hashCode(getValue(Magnitude.SINGLE));
}

public enum Magnitude
{
//This order is important, it should be in increasing magnitude.
SINGLE(1L, ""),
THOUSAND(1000L, "K"),
MILLION(1000_000L, "M"),
BILLION(1000_000_000L, "B"),
TRILION(1000_000_000_000L, "T"),
QUADRILLION(1000_000_000_000_000L, "P");

private final long factor;
private final String unitString;

Magnitude(long factor, String unitString)
{
this.factor = factor;
this.unitString = unitString;
}

long getFactor()
{
return factor;
}

public String getUnitString()
{
return unitString;
}
}
}
40 changes: 40 additions & 0 deletions src/main/java/io/airlift/units/MaxCount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.airlift.units;

import javax.validation.Constraint;
import javax.validation.Payload;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({METHOD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = MaxCountValidator.class)
public @interface MaxCount
{
String value();

String message() default "{io.airlift.units.MaxCount.message}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
41 changes: 41 additions & 0 deletions src/main/java/io/airlift/units/MaxCountValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.airlift.units;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class MaxCountValidator
implements ConstraintValidator<MaxCount, Count>
{
private Count max;

@Override
public void initialize(MaxCount count)
{
this.max = Count.valueOf(count.value());
}

@Override
public boolean isValid(Count count, ConstraintValidatorContext context)
{
return (count == null) || (count.compareTo(max) <= 0);
}

@Override
public String toString()
{
return "max:" + max;
}
}
40 changes: 40 additions & 0 deletions src/main/java/io/airlift/units/MinCount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.airlift.units;

import javax.validation.Constraint;
import javax.validation.Payload;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({METHOD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = MinCountValidator.class)
public @interface MinCount
{
String value();

String message() default "{io.airlift.units.MinCount.message}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
41 changes: 41 additions & 0 deletions src/main/java/io/airlift/units/MinCountValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.airlift.units;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class MinCountValidator
implements ConstraintValidator<MinCount, Count>
{
private Count min;

@Override
public void initialize(MinCount dataSize)
{
this.min = Count.valueOf(dataSize.value());
}

@Override
public boolean isValid(Count count, ConstraintValidatorContext context)
{
return (count == null) || (count.compareTo(min) >= 0);
}

@Override
public String toString()
{
return "min:" + min;
}
}
Loading

0 comments on commit 9f22896

Please sign in to comment.