Skip to content

Commit

Permalink
Support floating-point numbers without a fractional part (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
bw-hro authored Dec 9, 2023
1 parent b0cbf3f commit f6e4fcd
Show file tree
Hide file tree
Showing 6 changed files with 381 additions and 1 deletion.
10 changes: 10 additions & 0 deletions src/main/java/io/webthings/webthing/Property.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,14 @@ public Thing getThing() {
public JSONObject getMetadata() {
return this.metadata;
}

/**
* Get the base type of this properties value.
*
* @return The base type.
*/
public Class<T> getBaseType()
{
return value.getBaseType();
}
}
7 changes: 7 additions & 0 deletions src/main/java/io/webthings/webthing/Thing.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import io.webthings.webthing.errors.PropertyError;
Expand Down Expand Up @@ -399,6 +400,12 @@ public <T> void setProperty(String propertyName, T value)
return;
}

Optional<Object> converted = Utils.checkIfBaseTypeConversionIsRequired(prop.getBaseType(), value);
if(converted.isPresent()) {
setProperty(propertyName, converted.get());
return;
}

prop.setValue(value);
}

Expand Down
30 changes: 30 additions & 0 deletions src/main/java/io/webthings/webthing/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

public class Utils {
/**
Expand Down Expand Up @@ -61,4 +65,30 @@ public static List<String> getAddresses() {
Collections.sort(ret);
return ret;
}

static Map<Class<?>, Function<Number, Object>> createNumberConverters()
{
Map<Class<?>, Function<Number, Object>> converters = new HashMap<>();
converters.put(Integer.class, n -> n.intValue());
converters.put(Long.class, n -> n.longValue());
converters.put(Float.class, n -> n.floatValue());
converters.put(Double.class, n -> n.doubleValue());
return converters;
}
final static Map<Class<?>, Function<Number, Object>> supportedNumberConverters = createNumberConverters();

/**
* Performes a type conversion when required.
* @param baseTypeClass The Class of the base type to check agains
* @param value Value that might need conversion
* @return Optional containing converted value or empty Optional when no conversion was required.
*/
public static <T> Optional<Object> checkIfBaseTypeConversionIsRequired(Class<?> baseTypeClass, T value) {
if(value instanceof Number && baseTypeClass != value.getClass()) {
if(Number.class.isAssignableFrom(baseTypeClass)) {
return Optional.ofNullable(supportedNumberConverters.get(baseTypeClass)).map(f -> f.apply((Number) value));
}
}
return Optional.empty();
}
}
66 changes: 65 additions & 1 deletion src/main/java/io/webthings/webthing/Value.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.webthings.webthing;

import java.util.Objects;
import java.util.Observable;
import java.util.function.Consumer;

Expand All @@ -16,7 +17,17 @@
* @author Tim Hinkes ([email protected])
*/
public class Value<T> extends Observable {

static class BaseTypeHelper
{
static <BT> Class<BT> derive(final BT initialValue) {
Objects.requireNonNull(initialValue, "Can not derive base type from null.");
return (Class<BT>) initialValue.getClass();
}
}

private final Consumer<T> valueForwarder;
private final Class<T> baseType;
private T lastValue;

/**
Expand All @@ -28,7 +39,20 @@ public class Value<T> extends Observable {
* @param initialValue The initial value
*/
public Value(final T initialValue) {
this(initialValue, null);
this(BaseTypeHelper.derive(initialValue), initialValue, null);
}

/**
* Create a read only value that can only be updated by a Thing's reading.
* Initial value will be set to null.
* <p>
* Example: A sensor is updating its reading, but the reading cannot be set
* externally.
*
* @param baseType The Class of the values base type
*/
public Value(final Class<T> baseType) {
this(baseType, null, null);
}

/**
Expand All @@ -41,10 +65,50 @@ public Value(final T initialValue) {
* thing
*/
public Value(final T initialValue, final Consumer<T> valueForwarder) {
this(BaseTypeHelper.derive(initialValue), initialValue, valueForwarder);
}

/**
* Create a writable value that can be set to a new value.
* Initial value will be set to null.
* <p>
* Example: A light that can be switched off by setting this to false.
*
* @param baseType The Class of the values base type
* @param valueForwarder The method that updates the actual value on the
* thing
*/
public Value(final Class<T> baseType, final Consumer<T> valueForwarder) {
this(baseType, null, valueForwarder);
}

/**
* Create a writable value that can be set to a new value.
* <p>
* Example: A light that can be switched off by setting this to false.
*
* @param baseType The Class of the values base type
* @param initialValue The initial value
* @param valueForwarder The method that updates the actual value on the
* thing
*/
public Value(final Class<T> baseType, final T initialValue, final Consumer<T> valueForwarder) {
Objects.requireNonNull(baseType, "The base type of a value must not be null.");
this.baseType = baseType;
this.lastValue = initialValue;
this.valueForwarder = valueForwarder;
}

/**
* Get the base type of this value.
*
* @return The base type.
*/
public Class<T> getBaseType()
{
return this.baseType;
}

/**
* Set a new Value for this thing.
* <p>
Expand Down
217 changes: 217 additions & 0 deletions src/test/java/io/webthings/webthing/ThingTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package io.webthings.webthing;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Test;

import io.webthings.webthing.errors.PropertyError;

public class ThingTest {

Object simulateHttpPutProperty(String key, String jsonBody) {
JSONObject json = new JSONObject(jsonBody);
return json.get(key);
}

@Test
public void itSupportsIntegerValues() throws PropertyError
{
// given
Thing thing = new Thing("urn:dev:test-123", "My TestThing");

Value<Integer> value = new Value<>(42, v -> System.out.println("value: " + v));
thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "integer")));

// when updating property, then
assertEquals(42, value.get().intValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Integer.MIN_VALUE+"}"));
assertEquals(Integer.MIN_VALUE, value.get().intValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Integer.MAX_VALUE+"}"));
assertEquals(Integer.MAX_VALUE, value.get().intValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}"));
assertEquals(4, value.get().intValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}"));
assertEquals(4, value.get().intValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}"));
assertEquals(0, value.get().intValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123}"));
assertEquals(-123, value.get().intValue());
}

@Test
public void itSupportsLongValues() throws PropertyError
{
// given
Thing thing = new Thing("urn:dev:test-123", "My TestThing");

Value<Long> value = new Value<>(42l, v -> System.out.println("value: " + v));
thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "integer")));

// when updating property, then
assertEquals(42, value.get().longValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Long.MIN_VALUE+"}"));
assertEquals(Long.MIN_VALUE, value.get().longValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Long.MAX_VALUE+"}"));
assertEquals(Long.MAX_VALUE, value.get().longValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}"));
assertEquals(4, value.get().longValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}"));
assertEquals(4, value.get().longValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}"));
assertEquals(0, value.get().longValue());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123}"));
assertEquals(-123, value.get().longValue());
}

@Test
public void itSupportsFloatValues() throws PropertyError
{
// given
Thing thing = new Thing("urn:dev:test-123", "My TestThing");

Value<Float> value = new Value<>(42.0123f, v -> System.out.println("value: " + v));
thing.addProperty(new Property<>(thing, "p", value,
new JSONObject().put("type", "number")));

// when updating property, then
assertEquals(42.0123f, value.get().floatValue(), 0.00001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Float.MIN_VALUE+"}"));
assertEquals(Float.MIN_VALUE, value.get().floatValue(), 0.00001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Float.MAX_VALUE+"}"));
assertEquals(Float.MAX_VALUE, value.get().floatValue(), 0.00001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}"));
assertEquals(4.2f, value.get().floatValue(), 0.00001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}"));
assertEquals(4f, value.get().floatValue(), 0.00001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}"));
assertEquals(0f, value.get().floatValue(), 0.00001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123.456}"));
assertEquals(-123.456f, value.get().floatValue(), 0.00001);
}

@Test
public void itSupportsDoubleValues() throws PropertyError
{
// given
Thing thing = new Thing("urn:dev:test-123", "My TestThing");

Value<Double> value = new Value<>(42.0123, v -> System.out.println("value: " + v));
thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "number")));

// when updating property, then
assertEquals(42.0123, value.get(), 0.00001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Double.MIN_VALUE+"}"));
assertEquals(Double.MIN_VALUE, value.get(), 0.0000000001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Double.MAX_VALUE+"}"));
assertEquals(Double.MAX_VALUE, value.get(), 0.0000000001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}"));
assertEquals(4.2, value.get(), 0.00001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}"));
assertEquals(4, value.get(), 0.00001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}"));
assertEquals(0, value.get(), 0.00001);

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123.456}"));
assertEquals(-123.456, value.get(), 0.00001);
}

@Test
public void itSupportsObjectValues() throws PropertyError
{
// given
Thing thing = new Thing("urn:dev:test-123", "My TestThing");

Value<JSONObject> value = new Value<>(new JSONObject().put("key1", "val1").put("key2", "val2"),
v -> System.out.println("value: " + v));
thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "object")));

// when updating property, then
Map<String, Object> expectedMap = new HashMap<>();
expectedMap.put("key1", "val1");
expectedMap.put("key2", "val2");
assertEquals(expectedMap, value.get().toMap());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":{\"key3\":\"val3\"}}"));
assertEquals(Collections.singletonMap("key3", "val3"), value.get().toMap());
}

@Test
public void itSupportsArrayValues() throws PropertyError
{
// given
Thing thing = new Thing("urn:dev:test-123", "My TestThing");

Value<JSONArray> value = new Value<>(new JSONArray("[1,2,3]"), v -> System.out.println("value: " + v));
thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "array")));

// when updating property, then
assertEquals(Arrays.asList(1,2,3), value.get().toList());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":[]}"));
assertEquals(Arrays.asList(), value.get().toList());
}

@Test
public void itSupportsStringValues() throws PropertyError
{
// given
Thing thing = new Thing("urn:dev:test-123", "My TestThing");

Value<String> value = new Value<>("the-initial-string", v -> System.out.println("value: " + v));
thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "string")));

// when updating property, then
assertEquals("the-initial-string", value.get());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":\"the-updated-string\"}"));
assertEquals("the-updated-string", value.get());
}

@Test
public void itSupportsBooleanValues() throws PropertyError
{
// given
Thing thing = new Thing("urn:dev:test-123", "My TestThing");

Value<Boolean> value = new Value<>(false, v -> System.out.println("value: " + v));
thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "boolean")));

// when updating property, then
assertFalse(value.get());

thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":true}"));
assertTrue(value.get());
}
}
Loading

0 comments on commit f6e4fcd

Please sign in to comment.