Skip to content

Commit

Permalink
Merge pull request #111 from kwhanalytics/issue_78_decimals
Browse files Browse the repository at this point in the history
Issue 78 - decimals/precision
  • Loading branch information
jonoxia authored Feb 7, 2019
2 parents 9d669fe + 32b1d05 commit 262c64a
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 16 deletions.
41 changes: 30 additions & 11 deletions oblib/data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,14 +444,15 @@ class Fact(object):
as either XML or JSON. A Fact provides a value for a certain concept within
a certain context, and can optionally provide units and a precision.
"""
def __init__(self, concept, context, unit, value, decimals=2):
def __init__(self, concept, context, unit, value, decimals=None, precision=None):
"""
Concept is the field name - it must match the schema definition.
Context is a reference to this fact's parent Context object.
Units is a string naming the unit, for example "kWh"
Value is a string, integer, or float
Decimals is used only for float types, it is the number of
digits
Value is a string, integer, or float.
For numeric types only, precision OR decimals (not both) can be specified.
Decimals is the number of digits past the decimal point; precision is
the total number of significant digits.
"""
# in case of xml the context ID will be rendered in the fact tag.
# in case of json the contexts' attributes will be copied to the
Expand All @@ -460,7 +461,15 @@ def __init__(self, concept, context, unit, value, decimals=2):
self.value = value
self.context = context
self.unit = unit

if decimals is not None and precision is not None:
raise OBException("Fact given both precision and decimals - use only one.")
self.decimals = decimals
self.precision = precision
# FUTURE TODO: decide if the Concept is numeric or non-numeric. If numeric,
# either require a decimals/precision or provide a default. If non-numeric,
# don't allow decimals/precision to be set.

# Fill in the id property with a UUID:
self.id = identifier.identifier() # Only used when exporting JSON

Expand All @@ -469,14 +478,13 @@ def _toXML(self):
Return the Fact as an XML element.
"""
attribs = {"contextRef": self.context.get_id()}
# TODO the "pure" part is probably wrong now.
# also the self.unit may not be correct unitRef? not sure
# TODO the self.unit may not be correct unitRef? not sure
if self.unit is not None:
attribs["unitRef"] = self.unit
if self.unit == "pure" or self.unit == "degrees":
attribs["decimals"] = "0"
else:
if self.decimals is not None:
attribs["decimals"] = str(self.decimals)
elif self.precision is not None:
attribs["precision"] = str(self.precision)
elem = Element(self.concept, attrib=attribs)
if self.unit == "pure":
elem.text = "%d" % self.value
Expand All @@ -493,6 +501,10 @@ def _toJSON(self):
aspects["concept"] = self.concept
if self.unit is not None:
aspects["unit"] = self.unit
if self.decimals is not None:
aspects["decimals"] = str(self.decimals)
elif self.precision is not None:
aspects["precision"] = str(self.precision)

if isinstance( self.value, datetime.datetime):
value_str = self.value.strftime("%Y-%m-%dT%H:%M:%S")
Expand Down Expand Up @@ -988,6 +1000,12 @@ def set(self, concept_name, value, **kwargs):

if "precision" in kwargs:
precision = kwargs.pop("precision")
else:
precision = None
if "decimals" in kwargs:
decimals = kwargs.pop("decimals")
else:
decimals = None

if not self.is_concept_writable(concept_name):
raise OBConceptException(
Expand Down Expand Up @@ -1029,8 +1047,9 @@ def set(self, concept_name, value, **kwargs):
table = self.get_table_for_concept(concept_name)
context = table.store_context(context) # dedupes, assigns ID

f = Fact(concept_name, context, unit_name, value)
# TODO pass in decimals? Fact expects decimals and "precision" is slightly different
f = Fact(concept_name, context, unit_name, value,
precision=precision,
decimals=decimals)

# self.facts is nested dict keyed first on table then on context ID
# and finally on concept:
Expand Down
44 changes: 42 additions & 2 deletions oblib/tests/test_data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import unittest
from data_model import OBInstance, Context, Hypercube, UNTABLE
from data_model import OBException, OBContextException
from datetime import datetime, date
from taxonomy import getTaxonomy
from lxml import etree
Expand Down Expand Up @@ -418,7 +419,7 @@ def test_conversion_to_json(self):
root = json.loads(jsonstring)

# should have 2 facts:
all_facts = root["facts"].values()
all_facts = list(root["facts"].values())
self.assertEqual( len(all_facts), 2)

# each should have expected 'value' and 'aspects':
Expand Down Expand Up @@ -648,7 +649,7 @@ def test_hypercube_rejects_context_with_unwanted_axes(self):
instant = datetime.now())
# InverterPowerLevelPercentAxis is a valid axis and this is a valid value for it,
# but the table that holds DeviceCost doesn't want this axis:
with self.assertRaises(Exception):
with self.assertRaises(OBContextException):
doc.validate_context("solar:DeviceCost", threeAxisContext)


Expand Down Expand Up @@ -774,4 +775,43 @@ def test_validate_values_for_enumerated_solar_data_types(self):
pass


def test_decimals_and_precision(self):
# if we set a fact and pass in a Decimals argument,
# then when we write out to JSON or XML we should see decimals there.
# Same with Precision.
# Trying to set both Decimals and Precision should give an error.
# If we don't set either, it should default to decimals=2.

doc = OBInstance("CutSheet", self.taxonomy)
now = datetime.now()
doc.set_default_context({"entity": "JUPITER",
"solar:TestConditionAxis": "solar:StandardTestConditionMember",
PeriodType.instant: now,
PeriodType.duration: "forever"
})

# If we set a fact that wants a duration context, it should use jan 1 - jan 31:
doc.set("solar:ModuleNameplateCapacity", "6.25", unit_name="W",
ProductIdentifierAxis = 1, precision = 3)

jsonstring = doc.to_JSON_string()
facts = json.loads(jsonstring)["facts"]

# TODO is supposed to be in aspects or not?
self.assertEqual(len(facts), 1)
self.assertEqual(list(facts.values())[0]["aspects"]["precision"], "3")

doc.set("solar:ModuleNameplateCapacity", "6.25", unit_name="W",
ProductIdentifierAxis = 1, decimals = 3)
jsonstring = doc.to_JSON_string()
facts = json.loads(jsonstring)["facts"]
self.assertEqual(len(facts), 1)
self.assertEqual(list(facts.values())[0]["aspects"]["decimals"], "3")

# Trying to set both decimals and precision should raise an error
with self.assertRaises(OBException):
doc.set("solar:ModuleNameplateCapacity", "6.25", unit_name="W",
ProductIdentifierAxis = 1, decimals = 3, precision=3)



3 changes: 0 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,3 @@ Sphinx==1.8.2
sphinx-rtd-theme==0.4.2
sphinxcontrib-websupport==1.1.0

# pyinstaller
pyinstaller==3.4

0 comments on commit 262c64a

Please sign in to comment.