Ticket #17754: measure.2.py

File measure.2.py, 15.6 KB (added by Riccardo Di Virgilio, 12 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
Back to Top