Skip to content

Commit

Permalink
Merge pull request #228 from nielstron/227-tojson
Browse files Browse the repository at this point in the history
Add dict/json import/export to Quantities, Units and Entities
  • Loading branch information
nielstron committed May 10, 2023
2 parents c593f8d + 13081bb commit 9dafd76
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 4 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Expand Up @@ -41,6 +41,8 @@ script:
- coverage run -a --source=quantulum3 setup.py test -s quantulum3.tests.test_parse_ranges
# Test language specific non-classifier tasks
- coverage run -a --source=quantulum3 setup.py test -s quantulum3._lang.en_US.tests.extract_spellout_values
# Test class methods
- coverage run -a --source=quantulum3 setup.py test -s quantulum3.tests.test_classes
# Test requirements.txt for classifier requirements
- pip install -r requirements_classifier.txt
# Lint package, now that all requirements are installed
Expand Down
35 changes: 34 additions & 1 deletion README.md
Expand Up @@ -116,6 +116,37 @@ dimensionality:
Unit(name="kilometre per second", entity=Entity("speed"), uri=None)
```

### Export/Import

Entities, Units and Quantities can be exported to dictionaries and JSON strings:

```pycon
>>> quant = parser.parse('I want 2 liters of wine')
>>> quant[0].to_dict()
{'value': 2.0, 'unit': 'litre', "entity": "volume", 'surface': '2 liters', 'span': (7, 15), 'uncertainty': None, 'lang': 'en_US'}
>>> quant[0].to_json()
'{"value": 2.0, "unit": "litre", "entity": "volume", "surface": "2 liters", "span": [7, 15], "uncertainty": null, "lang": "en_US"}'
```

By default, only the unit/entity name is included in the exported dictionary, but these can be included:

```pycon
>>> quant = parser.parse('I want 2 liters of wine')
>>> quant[0].to_dict(include_unit_dict=True, include_entity_dict=True) # same args apply to .to_json()
{'value': 2.0, 'unit': {'name': 'litre', 'surfaces': ['cubic decimetre', 'cubic decimeter', 'litre', 'liter'], 'entity': {'name': 'volume', 'dimensions': [{'base': 'length', 'power': 3}], 'uri': 'Volume'}, 'uri': 'Litre', 'symbols': ['l', 'L', 'ltr', 'ℓ'], 'dimensions': [{'base': 'decimetre', 'power': 3}], 'original_dimensions': [{'base': 'litre', 'power': 1, 'surface': 'liters'}], 'currency_code': None, 'lang': 'en_US'}, 'entity': 'volume', 'surface': '2 liters', 'span': (7, 15), 'uncertainty': None, 'lang': 'en_US'}
```

Similar export syntax applies to exporting Unit and Entity objects.

You can import Entity, Unit and Quantity objects from dictionaries and JSON. This requires that the object was exported with `include_unit_dict=True` and `include_entity_dict=True` (as appropriate):

```pycon
>>> quant_dict = quant[0].to_dict(include_unit_dict=True, include_entity_dict=True)
>>> quant = Quantity.from_dict(quant_dict)
>>> ent_json = "{'name': 'volume', 'dimensions': [{'base': 'length', 'power': 3}], 'uri': 'Volume'}"
>>> ent = Entity.from_json(ent_json)
```

### Disambiguation

If the parser detects an ambiguity, a classifier based on the WikiPedia
Expand Down Expand Up @@ -145,7 +176,7 @@ In addition to that, the classifier is trained on the most similar words to
all of the units surfaces, according to their distance in [GloVe](https://nlp.stanford.edu/projects/glove/)
vector representation.

## Spoken version
### Spoken version

Quantulum classes include methods to convert them to a speakable unit.

Expand All @@ -156,6 +187,8 @@ ten billion gigawatts
Gimme ten billion dollars now and also one terawatt and zero point five joules!
```



### Manipulation

While quantities cannot be manipulated within this library, there are
Expand Down
158 changes: 155 additions & 3 deletions quantulum3/classes.py
Expand Up @@ -4,13 +4,48 @@
:mod:`Quantulum` classes.
"""

import json
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Tuple

from . import speak


class JSONMIxin(ABC):
@abstractmethod
def to_dict(self):
pass # pragma: no cover

def to_json(self, *args, **kwargs) -> str:
"""
Create a JSON representation of this object.
:param args: Arguments to pass to the to_dict method
:param kwargs: Keyword arguments to pass to the to_dict method
:return: JSON representation of this object
"""
return json.dumps(self.to_dict(*args, **kwargs))

@classmethod
@abstractmethod
def from_dict(cls, ddict: Dict):
pass # pragma: no cover

@classmethod
def from_json(cls, json_str: str):
"""
Create an object from a JSON string.
:param json_str: JSON string to convert to an object
:return: Object created from the JSON string
"""
return cls.from_dict(json.loads(json_str))


###############################################################################
class Entity(object):
class Entity(JSONMIxin, object):
"""
Class for an entity (e.g. "volume").
"""
Expand Down Expand Up @@ -42,9 +77,36 @@ def __ne__(self, other):
def __hash__(self):
return hash(repr(self))

def to_dict(self) -> Dict:
"""
Create a dictionary representation of this entity.
:return: Dictionary representation of this entity
"""
return {
"name": self.name,
"dimensions": self.dimensions,
"uri": self.uri,
}

@classmethod
def from_dict(cls, ddict: Dict) -> "Entity":
"""
Create an entity from a dictionary representation.
:param ddict: Dictionary representation of an entity (as produced by to_dict)
:return: Entity created from the dictionary representation
"""
return cls(
name=ddict["name"],
dimensions=ddict["dimensions"],
uri=ddict["uri"],
)


###############################################################################
class Unit(object):
class Unit(JSONMIxin, object):
"""
Class for a unit (e.g. "gallon").
"""
Expand Down Expand Up @@ -111,11 +173,57 @@ def __ne__(self, other):
def __hash__(self):
return hash(repr(self))

def to_dict(self, include_entity_dict: bool = False) -> Dict:
"""
Create a dictionary representation of this unit.
:param include_entity: When False, just the name of the entity is included, when True the full entity is included. Default is False.
:return: Dictionary representation of this unit
"""
ddict = {
"name": self.name,
"surfaces": self.surfaces,
"entity": self.entity.name,
"uri": self.uri,
"symbols": self.symbols,
"dimensions": self.dimensions,
"original_dimensions": self.original_dimensions,
"currency_code": self.currency_code,
"lang": self.lang,
}

if include_entity_dict:
ddict["entity"] = self.entity.to_dict()

return ddict

@classmethod
def from_dict(cls, ddict: Dict) -> "Unit":
"""
Create a unit from a dictionary representation.
:param ddict: Dictionary representation of a unit (as produced by to_dict)
:return: Unit created from the dictionary representation
"""
return cls(
name=ddict["name"],
surfaces=ddict["surfaces"],
entity=Entity.from_dict(ddict["entity"]),
uri=ddict["uri"],
symbols=ddict["symbols"],
dimensions=ddict["dimensions"],
original_dimensions=ddict["original_dimensions"],
currency_code=ddict["currency_code"],
lang=ddict["lang"],
)


###############################################################################


class Quantity(object):
class Quantity(JSONMIxin, object):
"""
Class for a quantity (e.g. "4.2 gallons").
"""
Expand Down Expand Up @@ -183,3 +291,47 @@ def to_spoken(self, lang=None):
:return: Speakable version of this quantity
"""
return speak.quantity_to_spoken(self, lang or self.lang)

def to_dict(
self, include_unit_dict: bool = False, include_entity_dict: bool = False
) -> Dict:
"""
Create a dictionary representation of this quantity
:param include_unit: When False, just the name of the unit is included, when True, the full unit is included. Defaults to False
:param include_entity: When False, just the name of the entity is included, when True, the full entity is included. Defaults to False. Only used when include_unit is True.
:return: Dictionary representation of this quantity
"""
ddict = {
"value": self.value,
"unit": self.unit.name,
"entity": self.unit.entity.name,
"surface": self.surface,
"span": self.span,
"uncertainty": self.uncertainty,
"lang": self.lang,
}

if include_unit_dict:
ddict["unit"] = self.unit.to_dict(include_entity_dict)

return ddict

@classmethod
def from_dict(cls, ddict: Dict) -> "Quantity":
"""
Create a quantity from a dictionary representation.
:param ddict: Dictionary representation of a quantity (as produced by to_dict)
:return: Quantity created from the dictionary representation
"""
return cls(
value=ddict["value"],
unit=Unit.from_dict(ddict["unit"]),
surface=ddict["surface"],
span=tuple(ddict["span"]),
uncertainty=ddict["uncertainty"],
lang=ddict["lang"],
)
100 changes: 100 additions & 0 deletions quantulum3/tests/test_classes.py
@@ -0,0 +1,100 @@
import unittest

from ..classes import Entity, Quantity, Unit


class TestClasses(unittest.TestCase):
def setUp(self):
self.e = Entity(name="test_entity", dimensions=list(), uri="test_uri")
self.u = Unit(
name="test_unit",
entity=self.e,
surfaces=["test_surface"],
uri="test_uri",
symbols=["test_symbol"],
)
self.q = Quantity(
1.0,
self.u,
surface="test_surface",
span=(0, 1),
uncertainty=0.1,
lang="en_US",
)

def test_entity_to_dict(self):
entity_dict = self.e.to_dict()
self.assertIsInstance(entity_dict, dict)
self.assertEqual(entity_dict["name"], self.e.name)
self.assertEqual(entity_dict["dimensions"], self.e.dimensions)
self.assertEqual(entity_dict["uri"], self.e.uri)

def test_entity_to_json(self):
entity_json = self.e.to_json()
self.assertIsInstance(entity_json, str)

def test_entity_from_dict(self):
entity_dict = self.e.to_dict()
entity = Entity.from_dict(entity_dict)
self.assertEqual(entity, self.e)
self.assertIsInstance(entity, Entity)

def test_entity_from_json(self):
entity_json = self.e.to_json()
entity = Entity.from_json(entity_json)
self.assertIsInstance(entity, Entity)

def test_unit_to_dict(self):
unit_dict = self.u.to_dict()
self.assertIsInstance(unit_dict, dict)
self.assertEqual(unit_dict["name"], self.u.name)
self.assertEqual(unit_dict["entity"], self.u.entity.name)
self.assertEqual(unit_dict["surfaces"], self.u.surfaces)
self.assertEqual(unit_dict["uri"], self.u.uri)
self.assertEqual(unit_dict["symbols"], self.u.symbols)
self.assertEqual(unit_dict["dimensions"], self.u.dimensions)
self.assertEqual(unit_dict["currency_code"], self.u.currency_code)
self.assertEqual(unit_dict["original_dimensions"], self.u.original_dimensions)
self.assertEqual(unit_dict["lang"], self.u.lang)

def test_unit_to_json(self):
unit_json = self.u.to_json()
self.assertIsInstance(unit_json, str)

def test_unit_from_dict(self):
unit_dict = self.u.to_dict(include_entity_dict=True)
unit = Unit.from_dict(unit_dict)
self.assertEqual(unit, self.u)
self.assertIsInstance(unit, Unit)

def test_unit_from_json(self):
unit_json = self.u.to_json(include_entity_dict=True)
unit = Unit.from_json(unit_json)
self.assertEqual(unit, self.u)
self.assertIsInstance(unit, Unit)

def test_quantity_to_dict(self):
quantity_dict = self.q.to_dict()
self.assertIsInstance(quantity_dict, dict)
self.assertEqual(quantity_dict["value"], self.q.value)
self.assertEqual(quantity_dict["unit"], self.u.name)
self.assertEqual(quantity_dict["surface"], self.q.surface)
self.assertEqual(quantity_dict["span"], self.q.span)
self.assertEqual(quantity_dict["uncertainty"], self.q.uncertainty)
self.assertEqual(quantity_dict["lang"], self.q.lang)

def test_quantity_to_json(self):
quantity_json = self.q.to_json()
self.assertIsInstance(quantity_json, str)

def test_quantity_from_dict(self):
quantity_dict = self.q.to_dict(include_unit_dict=True, include_entity_dict=True)
quantity = Quantity.from_dict(quantity_dict)
self.assertEqual(quantity, self.q)
self.assertIsInstance(quantity, Quantity)

def test_quantity_from_json(self):
quantity_json = self.q.to_json(include_unit_dict=True, include_entity_dict=True)
quantity = Quantity.from_json(quantity_json)
self.assertEqual(quantity, self.q)
self.assertIsInstance(quantity, Quantity)

0 comments on commit 9dafd76

Please sign in to comment.