#!/usr/bin/env python
#
# units.py
"""
Provides a variety of units for use with pagesizes.
"""
#
# Copyright © 2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
# Based on reportlab.lib.pagesizes and reportlab.lib.units
# www.reportlab.co.uk
# Copyright ReportLab Europe Ltd. 2000-2017
# Copyright (c) 2000-2018, ReportLab Inc.
# All rights reserved.
# Licensed under the BSD License
#
# Includes data from en.wikipedia.org.
# Licensed under the Creative Commons Attribution-ShareAlike License
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.
#
# stdlib
import math
from decimal import ROUND_HALF_UP, Decimal
from typing import SupportsFloat, Union
# this package
from domdf_python_tools.doctools import prettify_docstrings
__all__ = [
"pt",
"inch",
"cm",
"mm",
"um",
"pc",
"pica",
"Unit",
"Unitpt",
"UnitInch",
"Unitcm",
"Unitmm",
"Unitum",
"Unitpc",
]
def _rounders(val_to_round: Union[str, int, float, Decimal], round_format: str) -> Decimal:
return Decimal(Decimal(val_to_round).quantize(Decimal(str(round_format)), rounding=ROUND_HALF_UP))
[docs]@prettify_docstrings
class Unit(float):
r"""
Represents a unit, such as a point.
Behaves much like a float (which it inherits from).
:bold-title:`Addition`
Units can be added to each other:
.. code-block:: python
>>> (3*mm) + (7*mm)
<Unit '10.000 mm': 28.346pt>
When adding different :class:`~domdf_python_tools.pagesizes.units.Unit` objects,
the result has the type of the former unit:
.. code-block:: python
>>> (2.54*cm) + inch
<Unit '5.080 cm': 144.000pt>
>>> inch + (2.54*cm)
<Unit '2.000 inch': 144.000pt>
:class:`~domdf_python_tools.pagesizes.units.Unit` objects can also be added to :class:`float` and :class:`int` objects:
.. code-block:: python
>>> (3*cm) + 7
<Unit '10.000 cm': 283.465pt>
>>> 7 + (3*cm)
<Unit '10.000 cm': 283.465pt>
:bold-title:`Subtraction`
Subtraction works the same as addition:
.. code-block:: python
>>> (17*mm) - (7*mm)
<Unit '10.000 mm': 28.346pt>
>>> (2.54*cm) - inch
<Unit '0.000 cm': 0.000pt>
>>> inch - (2.54*cm)
<Unit '0.000 inch': 0.000pt>
>>> (17*cm) - 7
<Unit '10.000 cm': 283.465pt>
>>> 17 - (7*cm)
<Unit '10.000 cm': 283.465pt>
:bold-title:`Multiplication`
:class:`~domdf_python_tools.pagesizes.units.Unit` objects can only be multipled by
:class:`float` and :class:`int` objects:
.. code-block:: python
>>> (3*mm) * 3
<Unit '9.000 mm': 25.512pt>
>>> 3 * (3*mm)
<Unit '9.000 mm': 25.512pt>
>>> 3.5 * (3*mm)
<Unit '10.500 mm': 29.764pt>
Multiplication works either way round.
Multiplying by another :class:`~domdf_python_tools.pagesizes.units.Unit`
results in a :exc:`NotImplementedError`:
.. code-block:: python
>>> inch * (7*cm)
Traceback (most recent call last):
NotImplementedError: Multiplying a unit by another unit is not allowed.
:bold-title:`Division`
:class:`~domdf_python_tools.pagesizes.units.Unit`\s can only be divided by
:class:`float` and :class:`int` objects:
.. code-block:: python
>>> (3*mm) / 3
<Unit '1.000 mm': 2.835pt>
>>> (10*mm) / 2.5
<Unit '4.000 mm': 11.339pt>
Dividing by another unit results in a :exc:`NotImplementedError`:
.. code-block:: python
>>> inch / (7*cm)
Traceback (most recent call last):
NotImplementedError: Dividing a unit by another unit is not allowed.
Likewise, trying to divide a:class:`float` and :class:`int` object by a unit
results in a :exc:`NotImplementedError`:
.. code-block:: python
>>> 3 / (3*mm)
Traceback (most recent call last):
NotImplementedError: Dividing by a unit is not allowed.
:bold-title:`Powers`
Powers (using ``**``) are not officially supported.
:bold-title:`Modulo Division`
Modulo division of a :class:`~domdf_python_tools.pagesizes.units.Unit` by a
:class:`float` or :class:`int` object is allowed:
.. code-block:: python
>>> (3*mm) % 2.5
<Unit '0.500 mm': 1.417pt>
Dividing by a unit, or modulo division of two units, is not officially supported.
.. latex:clearpage::
"""
name: str = "pt"
_in_pt: float = 1
[docs] def __repr__(self):
value = _rounders(float(self), "0.000")
as_pt = _rounders(self.as_pt(), "0.000")
return f"<Unit '{value} {self.name}': {as_pt}pt>"
[docs] def __str__(self):
value = _rounders(float(self), "0.000")
as_pt = _rounders(self.as_pt(), "0.000")
return f"<Unit '{value}\u205F{self.name}': {as_pt}pt>"
[docs] def __mul__(self, other: Union[float, "Unit"]) -> "Unit":
if isinstance(other, Unit):
raise NotImplementedError("Multiplying a unit by another unit is not allowed.")
return self.__class__(super().__mul__(other))
__rmul__ = __mul__
[docs] def __truediv__(self, other: Union[float, "Unit"]) -> "Unit":
if isinstance(other, Unit):
raise NotImplementedError("Dividing a unit by another unit is not allowed.")
return self.__class__(super().__truediv__(other))
[docs] def __floordiv__(self, other: Union[float, "Unit"]) -> "Unit":
if isinstance(other, Unit):
raise NotImplementedError("Dividing a unit by another unit is not allowed.")
return self.__class__(super().__floordiv__(other))
[docs] def __eq__(self, other: object) -> bool:
if isinstance(other, Unit):
if self._in_pt != 1:
self_value = self.as_pt()
else:
self_value = self
if other._in_pt != 1:
other_value = other.as_pt()
else:
other_value = other
return math.isclose(float(self_value), float(other_value), abs_tol=1e-8)
else:
return super().__eq__(other)
[docs] def __mod__(self, other: Union[float, "Unit"]) -> "Unit":
if isinstance(other, Unit):
raise NotImplementedError("Modulo division of a unit by another unit is not allowed.")
return self.__class__(super().__mod__(other))
[docs] def __pow__(self, power, modulo=None):
raise NotImplementedError("Powers are not supported for units.")
[docs] def __rtruediv__(self, other):
raise NotImplementedError("Dividing by a unit is not allowed.")
__rdiv__ = __rtruediv__
[docs] def __add__(self, other: Union[float, "Unit"]) -> "Unit":
if isinstance(other, Unit):
return self.__class__.from_pt(float(self.as_pt()) + float(other.as_pt()))
else:
return self.__class__(super().__add__(other))
__radd__ = __add__
[docs] def __sub__(self, other: Union[float, "Unit"]) -> "Unit":
if isinstance(other, Unit):
return self.__class__.from_pt(float(self.as_pt()) - float(other.as_pt()))
else:
return self.__class__(super().__sub__(other))
[docs] def __rsub__(self, other: Union[float, "Unit"]) -> "Unit":
if isinstance(other, Unit): # pragma: no cover (sub should be called instead)
return self.__class__.from_pt(float(other.as_pt()) - float(self.as_pt()))
else:
return self.__class__(super().__rsub__(other))
[docs] def as_pt(self) -> "Unit":
"""
Returns the unit in point.
"""
return Unit(float(_rounders(float(self) * self._in_pt, "0.000000")))
[docs] @classmethod
def from_pt(cls, value: float) -> "Unit":
"""
Construct a :class:`~.Unit` object from a value in point.
:param value:
"""
return cls(value / cls._in_pt)
[docs] def __call__(self, value: Union[SupportsFloat, str, bytes, bytearray] = 0.0) -> "Unit":
"""
Returns an instance of the :class:`Unit` with the given value.
:param value:
"""
return self.__class__(value)
[docs]class Unitpt(Unit):
"""
Point.
"""
name = "pt"
_in_pt = 1
[docs]class UnitInch(Unit):
"""
Inch.
"""
name = "inch"
_in_pt = 72.0
[docs]class Unitcm(Unit):
"""
Centimetres.
"""
name = "cm"
_in_pt = 28.3464566929
[docs]class Unitmm(Unit):
"""
Millimetres.
"""
name = "mm"
_in_pt = 2.83464566929
[docs]class Unitum(Unit):
"""
Micrometres.
"""
name = "µm"
_in_pt = 0.00283464566929
[docs]class Unitpc(Unit):
"""
Pica.
"""
name = "pc"
_in_pt = 12.0
# Units
pt = Unitpt(1) #: Point
inch = UnitInch(1) #: Inch
cm = Unitcm(1) #: Centimetre
mm = Unitmm(1) #: Millimetre
um = Unitum(1) #: Micrometre
pc = pica = Unitpc(1) #: Pica