Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,25 @@
# such notice(s) shall fulfill the requirements of that article.
# *********************************************************************

import math
from decimal import Decimal, ROUND_HALF_UP

from fiftyone_pipeline_engines.aspectdata import AspectData
from fiftyone_pipeline_core.aspectproperty_value import AspectPropertyValue

from fiftyone_pipeline_engines.missingproperty_service import MissingPropertyService

from .constants import RESULTS_CLOSED

# Number of decimal places used when converting WKB geometry
# properties (e.g. Areas) to their WKT string form.
WKT_DECIMAL_PLACES = 16
# Latitude and Longitude are given to this many decimal places, as the
# native code gives them in text and in the WKT of Areas.
COORDINATE_DECIMAL_PLACES = 3

# The step a coordinate is rounded to, worked out from the places above so
# that the number of places is written down once.
COORDINATE_QUANTUM = Decimal(1).scaleb(-COORDINATE_DECIMAL_PLACES)

COORDINATE_PROPERTIES = ("latitude", "longitude")


class OnPremiseMissingPropertyService(MissingPropertyService):
Expand Down Expand Up @@ -166,6 +175,26 @@ def ip_address_value(self, value):

return AspectPropertyValue(no_value_message="The IP address type is invalid.")

@staticmethod
def round_coordinate(value):

"""!
A latitude or longitude to COORDINATE_DECIMAL_PLACES places.

A value half way between two places rounds away from zero, as the
native code rounds it, where Python's own round() would round it to
the nearer even place. A value that rounds to zero from below is
given as zero without a sign, because the native code writes the
same value as text without one. A value that is not finite is given
as it is, having no places to round.
"""

if math.isfinite(value) is False:
return value
rounded = float(Decimal(value).quantize(
COORDINATE_QUANTUM, rounding=ROUND_HALF_UP))
return 0.0 if rounded == 0 else rounded

def single_value(self, value, convert=None):

"""!
Expand Down Expand Up @@ -237,9 +266,15 @@ def get_internal(self, key):
return self.weighted_list_to_value(
results.getValuesAsWeightedIntegerList(target))

# Only the coordinates are rounded.
round_coordinate = (
self.round_coordinate if key in COORDINATE_PROPERTIES
else None)

if property_type in ("weighteddouble", "weightedsingle"):
return self.weighted_list_to_value(
results.getValuesAsWeightedDoubleList(target))
results.getValuesAsWeightedDoubleList(target),
convert=round_coordinate)

if property_type == "weightedbool":
return self.weighted_list_to_value(
Expand Down Expand Up @@ -269,7 +304,8 @@ def get_internal(self, key):
return self.single_value(results.getValueAsInteger(target))

if property_type in ("double", "single"):
return self.single_value(results.getValueAsDouble(target))
return self.single_value(
results.getValueAsDouble(target), convert=round_coordinate)

if property_type == "bool":
return self.single_value(results.getValueAsBool(target))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@

"""
IP Intelligence Latitude and Longitude are given to 3 decimal places, as the
value types page of the specification says.
"""

import math
import unittest

from fiftyone_ipintelligence_onpremise import swig_data
from fiftyone_ipintelligence_onpremise.swig_data import SwigData


class Weighted:

def __init__(self, value, weight):
self.value = value
self.weight = weight

def getValue(self):
return self.value

def getWeight(self):
return self.weight


class WeightedList:

def __init__(self, items):
self.items = items

def hasValue(self):
return True

def getValue(self):
return self.items


class Single:

def __init__(self, value):
self.value = value

def hasValue(self):
return True

def getValue(self):
return self.value


class FakeResults:

def getValuesAsWeightedDoubleList(self, name):
return WeightedList([Weighted(18.542741775512695, 65535),
Weighted(73.91278076171875, 100)])

def getValueAsDouble(self, name):
return Single(18.542741775512695)


class FakeElement:
datakey = "ip"

def __init__(self, type_name, name):
self.properties = {name.lower(): {"name": name, "type": type_name}}


def read(type_name, name):
data = SwigData(FakeElement(type_name, name), FakeResults())
return data.get_internal(name.lower()).value()


class DecimalPlacesTests(unittest.TestCase):

def test_weighted_latitude_and_longitude_have_three_places(self):
for type_name in ("weighteddouble", "weightedsingle"):
for name in ("Latitude", "Longitude"):
values = [item["value"] for item in read(type_name, name)]
self.assertEqual([18.543, 73.913], values, (type_name, name))

def test_weights_are_unchanged(self):
weights = [item["weighting"] for item in read("weighteddouble", "Latitude")]
self.assertEqual([65535, 100], weights)

def test_single_latitude_has_three_places(self):
for type_name in ("double", "single"):
self.assertEqual(18.543, read(type_name, "Latitude"))

def test_other_numbers_are_not_rounded(self):
values = [item["value"] for item in read("weighteddouble", "Accuracy")]
self.assertEqual([18.542741775512695, 73.91278076171875], values)

def test_the_unused_sixteen_place_setting_is_gone(self):
self.assertFalse(hasattr(swig_data, "WKT_DECIMAL_PLACES"))

def test_a_half_rounds_away_from_zero(self):
for value, expected in ((0.0625, 0.063),
(-0.0625, -0.063),
(51.3125, 51.313)):
self.assertEqual(expected, SwigData.round_coordinate(value), value)

def test_a_value_that_rounds_to_zero_from_below_has_no_sign(self):
for value in (-0.0004, -0.0):
rounded = SwigData.round_coordinate(value)
self.assertEqual(0.0, rounded, value)
self.assertFalse(math.copysign(1.0, rounded) < 0,
f"{value} was given as minus zero")

def test_a_value_that_is_not_finite_is_kept(self):
infinity = float("inf")
self.assertEqual(infinity, SwigData.round_coordinate(infinity))
self.assertEqual(-infinity, SwigData.round_coordinate(-infinity))
self.assertTrue(math.isnan(SwigData.round_coordinate(float("nan"))))


if __name__ == "__main__":
unittest.main()
Loading