From 898e9b3042e58796341420510daefa3d0dc0d81a Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 22:56:26 +0000 Subject: [PATCH 1/4] Test that IP Intelligence latitude and longitude have three decimal places --- .../tests/test_swig_data_decimal_places.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_swig_data_decimal_places.py diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_swig_data_decimal_places.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_swig_data_decimal_places.py new file mode 100644 index 00000000..f07ff636 --- /dev/null +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_swig_data_decimal_places.py @@ -0,0 +1,97 @@ + +""" +IP Intelligence Latitude and Longitude are given to 3 decimal places, as the +value types page of the specification says. +""" + +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")) + + +if __name__ == "__main__": + unittest.main() From aecedcbdca3b62f5b2ffca30c87078fa964b22e6 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 22:56:52 +0000 Subject: [PATCH 2/4] Give IP Intelligence latitude and longitude to three decimal places The native code gives the coordinates in text and in the WKT of Areas to three places, and the numbers read through the weighted and single value getters are rounded to match. The unused sixteen place setting is removed. --- .../swig_data.py | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/swig_data.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/swig_data.py index 40413cf3..d1e14789 100644 --- a/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/swig_data.py +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/swig_data.py @@ -27,9 +27,11 @@ 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 + +COORDINATE_PROPERTIES = ("latitude", "longitude") class OnPremiseMissingPropertyService(MissingPropertyService): @@ -166,6 +168,15 @@ 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 + """ + + return round(value, COORDINATE_DECIMAL_PLACES) + def single_value(self, value, convert=None): """! @@ -233,9 +244,15 @@ def get_internal(self, key): return self.weighted_list_to_value( results.getValuesAsWeightedIntegerList(name)) + # Only the coordinates are rounded. + round_coordinate = ( + self.round_coordinate if name.lower() in COORDINATE_PROPERTIES + else None) + if property_type in ("weighteddouble", "weightedsingle"): return self.weighted_list_to_value( - results.getValuesAsWeightedDoubleList(name)) + results.getValuesAsWeightedDoubleList(name), + convert=round_coordinate) if property_type == "weightedbool": return self.weighted_list_to_value( @@ -265,7 +282,8 @@ def get_internal(self, key): return self.single_value(results.getValueAsInteger(name)) if property_type in ("double", "single"): - return self.single_value(results.getValueAsDouble(name)) + return self.single_value( + results.getValueAsDouble(name), convert=round_coordinate) if property_type == "bool": return self.single_value(results.getValueAsBool(name)) From 232f971b037dfc96f833f3ab0955ec1700f37070 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Wed, 11 Nov 2026 12:49:49 +0000 Subject: [PATCH 3/4] Test that a half rounds away from zero and a coordinate rounding to zero has no sign --- .../tests/test_swig_data_decimal_places.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_swig_data_decimal_places.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_swig_data_decimal_places.py index f07ff636..3de26193 100644 --- a/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_swig_data_decimal_places.py +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_swig_data_decimal_places.py @@ -4,6 +4,7 @@ value types page of the specification says. """ +import math import unittest from fiftyone_ipintelligence_onpremise import swig_data @@ -92,6 +93,25 @@ def test_other_numbers_are_not_rounded(self): 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() From ad19be9a11ebf2acd3b42dba900eaf23102fee5c Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Wed, 11 Nov 2026 12:50:12 +0000 Subject: [PATCH 4/4] Round a half away from zero and give a coordinate that rounds to zero no sign --- .../swig_data.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/swig_data.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/swig_data.py index 7023700d..49fe459d 100644 --- a/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/swig_data.py +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/swig_data.py @@ -20,6 +20,9 @@ # 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 @@ -31,6 +34,10 @@ # 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") @@ -172,10 +179,21 @@ def ip_address_value(self, value): def round_coordinate(value): """! - A latitude or longitude to COORDINATE_DECIMAL_PLACES places + 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. """ - return round(value, COORDINATE_DECIMAL_PLACES) + 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):