Code

Ticket #17754: measure.2.py

File measure.2.py, 15.6 KB (added by riccardodivirgilio, 2 years ago)

Measure py with TEST

Line 
1# Copyright (c) 2007, Robert Coup <robert.coup@onetrackmind.co.nz>
2# All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without modification,
5# are permitted provided that the following conditions are met:
6#
7#   1. Redistributions of source code must retain the above copyright notice,
8#      this list of conditions and the following disclaimer.
9#
10#   2. Redistributions in binary form must reproduce the above copyright
11#      notice, this list of conditions and the following disclaimer in the
12#      documentation and/or other materials provided with the distribution.
13#
14#   3. Neither the name of Distance nor the names of its contributors may be used
15#      to endorse or promote products derived from this software without
16#      specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28#
29"""
30Distance and Area objects to allow for sensible and convienient calculation
31and conversions.
32
33Authors: Robert Coup, Justin Bronn
34
35Inspired by GeoPy (http://exogen.case.edu/projects/geopy/)
36and Geoff Biggs' PhD work on dimensioned units for robotics.
37"""
38__all__ = ['A', 'Area', 'D', 'Distance', 'V', 'Volume', 'W', 'Weight']
39from decimal import Decimal
40
41def is_number(obj):
42    return isinstance(obj, (int, float, long, Decimal))
43   
44def pretty_name(obj):
45    if obj.__class__ == type:
46        return obj.__name__
47    return obj.__class__.__name__
48
49class MeasureBase(object):
50   
51    STANDARD_UNIT  = None
52    ALIAS  = {}
53    UNITS  = {}
54    LALIAS = {}
55   
56    def __init__(self, default_unit=None, **kwargs):
57        # The base unit is in meters.
58        value, self._default_unit = self.default_units(kwargs)
59        setattr(self, self.STANDARD_UNIT, value)
60        if default_unit and isinstance(default_unit, str):
61            self._default_unit = default_unit
62           
63    def _get_standard(self):
64        return getattr(self, self.STANDARD_UNIT)
65       
66    def _set_standard(self, value):
67        setattr(self, self.STANDARD_UNIT, value)
68       
69    standard = property(_get_standard, _set_standard)
70           
71    def __getattr__(self, name):
72        if name in self.UNITS:
73            return self.standard / self.UNITS[name]
74        else:
75            raise AttributeError('Unknown unit type: %s' % name)
76
77    def __repr__(self):
78        return '%s(%s=%s)' % (pretty_name(self), self._default_unit, getattr(self, self._default_unit))
79
80    def __str__(self):
81        return '%s %s' % (getattr(self, self._default_unit), self._default_unit)
82
83    def __cmp__(self, other):
84        if isinstance(other, self.__class__):
85            return cmp(self.m, other.m)
86        else:
87            return NotImplemented
88
89    def __add__(self, other):
90        if isinstance(other, self.__class__):
91            return self.__class__(default_unit=self._default_unit, 
92                **{self.STANDARD_UNIT: (self.standard + other.standard)})
93        else:
94            raise TypeError('%(class)s must be added with %(class)s' % {"class":pretty_name(self)})
95
96    def __iadd__(self, other):
97        if isinstance(other, self.__class__):
98            self.standard += other.standard
99            return self
100        else:
101            raise TypeError('%(class)s must be added with %(class)s' % {"class":pretty_name(self)})
102
103    def __sub__(self, other):
104        if isinstance(other, self.__class__):
105            return self.__class__(default_unit=self._default_unit, 
106                **{self.STANDARD_UNIT: (self.standard - other.standard)})
107        else:
108            raise TypeError('%(class)s must be subtracted from %(class)s' % {"class":pretty_name(self)})
109
110    def __isub__(self, other):
111        if isinstance(other, self.__class__):
112            self.standard -= other.standard
113            return self
114        else:
115            raise TypeError('%(class)s must be subtracted from %(class)s' % {"class":pretty_name(self)})
116
117    def __mul__(self, other):
118        if is_number(other):
119            return self.__class__(default_unit=self._default_unit, 
120                **{self.STANDARD_UNIT: (self.standard * other)})
121        else:
122            raise TypeError('%(class)s must be multiplied with number' % {"class":pretty_name(self)})
123
124    def __imul__(self, other):
125        if is_number(other):
126            self.standard *= float(other)
127            return self
128        else:
129            raise TypeError('%(class)s must be multiplied with number' % {"class":pretty_name(self)})
130
131    def __rmul__(self, other):
132        return self * other
133
134    def __div__(self, other):
135        if isinstance(other, self.__class__):
136            return self.standard / other.standard     
137        if is_number(other):
138            return self.__class__(default_unit=self._default_unit, 
139                **{self.STANDARD_UNIT: (self.standard / other)})
140        else:
141            raise TypeError('%(class)s must be divided with number or %(class)s' % {"class":pretty_name(self)})
142
143    def __idiv__(self, other):
144        if is_number(other):
145            self.standard /= float(other)
146            return self
147        else:
148            raise TypeError('%(class)s must be divided with number' % {"class":pretty_name(self)})
149
150    def __nonzero__(self):
151        return bool(self.default)           
152   
153    def default_units(self, kwargs):
154        """
155        Return the unit value and the default units specified
156        from the given keyword arguments dictionary.
157        """
158        val = 0.0
159        for unit, value in kwargs.iteritems():
160            if not isinstance(value, float): value = float(value)
161            if unit in self.UNITS:
162                val += self.UNITS[unit] * value
163                default_unit = unit
164            elif unit in self.ALIAS:
165                u = self.ALIAS[unit]
166                val += self.UNITS[u] * value
167                default_unit = u
168            else:
169                lower = unit.lower()
170                if lower in self.UNITS:
171                    val += self.UNITS[lower] * value
172                    default_unit = lower
173                elif lower in self.LALIAS:
174                    u = self.LALIAS[lower]
175                    val += self.UNITS[u] * value
176                    default_unit = u
177                else:
178                    raise AttributeError('Unknown unit type: %s' % unit)
179        return val, default_unit
180
181    @classmethod
182    def unit_attname(cls, unit_str):
183        """
184        Retrieves the unit attribute name for the given unit string.
185        For example, if the given unit string is 'metre', 'm' would be returned.
186        An exception is raised if an attribute cannot be found.
187        """
188        lower = unit_str.lower()
189        if unit_str in cls.UNITS:
190            return unit_str
191        elif lower in cls.UNITS:
192            return lower
193        elif lower in cls.LALIAS:
194            return cls.LALIAS[lower]
195        else:
196            raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
197
198class Distance(MeasureBase):
199    STANDARD_UNIT = "m"
200    UNITS = {
201        'chain' : 20.1168,
202        'chain_benoit' : 20.116782,
203        'chain_sears' : 20.1167645,
204        'british_chain_benoit' : 20.1167824944,
205        'british_chain_sears' : 20.1167651216,
206        'british_chain_sears_truncated' : 20.116756,
207        'cm' : 0.01,
208        'british_ft' : 0.304799471539,
209        'british_yd' : 0.914398414616,
210        'clarke_ft' : 0.3047972654,
211        'clarke_link' : 0.201166195164,
212        'fathom' :  1.8288,
213        'ft': 0.3048,
214        'german_m' : 1.0000135965,
215        'gold_coast_ft' : 0.304799710181508,
216        'indian_yd' : 0.914398530744,
217        'inch' : 0.0254,
218        'km': 1000.0,
219        'link' : 0.201168,
220        'link_benoit' : 0.20116782,
221        'link_sears' : 0.20116765,
222        'm': 1.0,
223        'mi': 1609.344,
224        'mm' : 0.001,
225        'nm': 1852.0,
226        'nm_uk' : 1853.184,
227        'rod' : 5.0292,
228        'sears_yd' : 0.91439841,
229        'survey_ft' : 0.304800609601,
230        'um' : 0.000001,
231        'yd': 0.9144,
232        }
233
234    # Unit aliases for `UNIT` terms encountered in Spatial Reference WKT.
235    ALIAS = {
236        'centimeter' : 'cm',
237        'foot' : 'ft',
238        'inches' : 'inch',
239        'kilometer' : 'km',
240        'kilometre' : 'km',
241        'meter' : 'm',
242        'metre' : 'm',
243        'micrometer' : 'um',
244        'micrometre' : 'um',
245        'millimeter' : 'mm',
246        'millimetre' : 'mm',
247        'mile' : 'mi',
248        'yard' : 'yd',
249        'British chain (Benoit 1895 B)' : 'british_chain_benoit',
250        'British chain (Sears 1922)' : 'british_chain_sears',
251        'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated',
252        'British foot (Sears 1922)' : 'british_ft',
253        'British foot' : 'british_ft',
254        'British yard (Sears 1922)' : 'british_yd',
255        'British yard' : 'british_yd',
256        "Clarke's Foot" : 'clarke_ft',
257        "Clarke's link" : 'clarke_link',
258        'Chain (Benoit)' : 'chain_benoit',
259        'Chain (Sears)' : 'chain_sears',
260        'Foot (International)' : 'ft',
261        'German legal metre' : 'german_m',
262        'Gold Coast foot' : 'gold_coast_ft',
263        'Indian yard' : 'indian_yd',
264        'Link (Benoit)': 'link_benoit',
265        'Link (Sears)': 'link_sears',
266        'Nautical Mile' : 'nm',
267        'Nautical Mile (UK)' : 'nm_uk',
268        'US survey foot' : 'survey_ft',
269        'U.S. Foot' : 'survey_ft',
270        'Yard (Indian)' : 'indian_yd',
271        'Yard (Sears)' : 'sears_yd'
272        }
273    LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
274
275    def __mul__(self, other):
276        if isinstance(other, Area):
277            return Volume(default_unit=VOLUME_PREFIX + self._default_unit, 
278                **{VOLUME_PREFIX + self.STANDARD_UNIT: (self.standard * other.standard)})       
279        elif isinstance(other, self.__class__):
280            return Area(default_unit=AREA_PREFIX + self._default_unit, 
281                **{AREA_PREFIX + self.STANDARD_UNIT: (self.standard * other.standard)})
282        elif is_number(other):
283            return self.__class__(default_unit=self._default_unit, 
284                **{self.STANDARD_UNIT: (self.standard * other)})
285        else:
286            raise TypeError('%(distance)s must be multiplied with number, %(distance)s or %(area)s' % {
287                "distance" : pretty_name(self.__class__),
288                "area"     : pretty_name(Area),
289                })
290           
291AREA_PREFIX = "sq_"
292
293class Area(MeasureBase):
294    STANDARD_UNIT = AREA_PREFIX + Distance.STANDARD_UNIT
295    # Getting the square units values and the alias dictionary.
296    UNITS = dict([(AREA_PREFIX + k, v ** 2) for k, v in Distance.UNITS.items()])
297    ALIAS = dict([(k, AREA_PREFIX + v) for k, v in Distance.ALIAS.items()])
298    LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
299
300    def __div__(self, other):
301        if isinstance(other, self.__class__):
302            return self.standard / other.standard         
303        if isinstance(other, Distance):
304            return Distance(default_unit=self._default_unit[len(AREA_PREFIX):], 
305                **{Distance.STANDARD_UNIT: (self.standard / other.standard)})
306        elif is_number(other):
307            return self.__class__(default_unit=self._default_unit, 
308                **{self.STANDARD_UNIT: (self.standard / other)})
309        else:
310            raise TypeError('%(area)s must be divided with number, %(distance)s or %(area)s' % {
311                "distance" : pretty_name(Distance),
312                "area"     : pretty_name(self.__class__),
313                }) 
314           
315    def __mul__(self, other):
316        if isinstance(other, Distance):
317            return Volume(default_unit=VOLUME_PREFIX+self._default_unit[len(AREA_PREFIX):], 
318                **{Volume.STANDARD_UNIT: (self.standard * other.standard)})       
319        elif is_number(other):
320            return self.__class__(default_unit=self._default_unit, 
321                **{self.STANDARD_UNIT: (self.standard * other)})
322        else:
323            raise TypeError('%(area)s must be multiplied with number or %(distance)s' % {
324                "distance" : pretty_name(Distance),
325                "area"     : pretty_name(self.__class__),
326                }) 
327           
328VOLUME_PREFIX = "vol_"
329
330class Volume(MeasureBase):
331    STANDARD_UNIT = VOLUME_PREFIX + Distance.STANDARD_UNIT
332    # Getting the cube units values and the alias dictionary.
333    UNITS = dict([(VOLUME_PREFIX + k, v ** 3) for k, v in Distance.UNITS.items()])
334    ALIAS = dict([(k, VOLUME_PREFIX + v) for k, v in Distance.ALIAS.items()])
335    LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])   
336   
337    def __div__(self, other):
338        if isinstance(other, self.__class__):
339            return self.standard / other.standard         
340        if isinstance(other, Area):
341            return Distance(default_unit=self._default_unit[len(VOLUME_PREFIX):], 
342                **{Distance.STANDARD_UNIT: (self.standard / other.standard)})       
343        if isinstance(other, Distance):
344            return Area(default_unit=AREA_PREFIX+self._default_unit[len(VOLUME_PREFIX):], 
345                **{Area.STANDARD_UNIT: (self.standard / other.standard)})
346        elif is_number(other):
347            return self.__class__(default_unit=self._default_unit, 
348                **{self.STANDARD_UNIT: (self.standard / other)})
349        else:
350            raise TypeError('%(volume)s must be divided with number, %(distance)s, %(area)s or %(volume)s' % {
351                "distance" : pretty_name(Distance),
352                "area"     : pretty_name(Area),
353                "volume"   : pretty_name(self.__class__),
354                })     
355   
356class Weight(MeasureBase):
357   
358    DEFUALT_UNIT = "gr"
359   
360    UNITS = {
361        'mg':  1.0 / 1000,
362        'gr':  1.0,
363        'kg':  1.0 * 1000,
364        'q' :  1.0 * 1000 * 100,
365        'ton': 1.0 * 1000 * 1000,
366        }
367
368    # Unit aliases for `UNIT` terms encountered in Spatial Reference WKT.
369    ALIAS = {
370        'Milligram':  'mg',
371        'Gram':       'gr',
372        'Kilogram':   'kg',
373        'Quintal' :   'q',
374        'Ton' :       'ton',
375        }
376    LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
377
378# Shortcuts
379D = Distance
380A = Area
381V = Volume
382W = Weight
383
384
385if __name__ == '__main__':
386
387    from itertools import product
388   
389    CONSTRUCTORS = {
390        float: lambda cls, val: cls(val),
391        A:     lambda cls, val: cls(**{cls.STANDARD_UNIT:val}),
392        D:     lambda cls, val: cls(**{cls.STANDARD_UNIT:val}),
393        V:     lambda cls, val: cls(**{cls.STANDARD_UNIT:val}),
394    }
395   
396    classes = [D, A, V, float]
397   
398    combs = product(classes, repeat = 2)
399   
400    a_val = 10
401    b_val = 5       
402   
403    for a_class, b_class in combs:
404       
405        print "-----------%s-vs-%s-----------" % (a_class.__name__.title(), b_class.__name__.title())
406       
407        a = CONSTRUCTORS[a_class](a_class, a_val)
408        b = CONSTRUCTORS[b_class](b_class, b_val)
409       
410        for verbose, operator in (("+", "__add__"), ("-", "__sub__"), ("*", "__mul__"), ("/", "__div__")):
411           
412            desc = "%s %s %s" % (a, verbose, b)
413           
414            print desc.ljust(25), ">    ", 
415           
416            try:
417                print getattr(a, operator)(b)
418            except Exception, e:
419                print e   
420