2
import re, locale, math
3
from defaults.defaults import lang as defaults
4
from gettext import gettext as _
5
from gettext import ngettext
13
USE_FRACTIONS = FRACTIONS_NORMAL
15
class PossiblyCaseInsensitiveDictionary (dict):
17
transformations = ["lower","title","upper"]
19
def has_key (self, k):
20
if dict.has_key(self,k): return True
22
for t in self.transformations:
24
if dict.has_key(self,getattr(k,t)()): return True
27
def __getitem__ (self, k):
28
if dict.has_key(self,k):
29
return dict.__getitem__(self,k)
31
for t in self.transformations:
34
if dict.has_key(self,nk):
35
return dict.__getitem__(self,nk)
36
# Raise plain old error
37
dict.__getitem__(self,k)
51
'years':365.25*24*60*60,
52
#'decades':10*365.25*24*60*60,
53
#'centuries':100*365.25*24*60*60,
54
#'millenia':1000*365.25*24*60*60,
57
# this is a bit of a hackish attempt to make a matcher for all
58
# plural forms of time units. We use range(5) since as far as I
59
# can see, enumerating the forms for 0 through 5 should give you
60
# all possible forms in all languages.
62
# See http://www.sbin.org/doc/glibc/libc_8.html
63
time_units = [('seconds',[ngettext('second','seconds',n) for n in range(5)] + ['s.', 'sec', 'secs','s' ]),
64
# min. = abbreviation for minutes
65
('minutes',[_('min'),'min.','min','mins','m'] + [ngettext('minute','minutes',n) for n in range(5)]),
66
# hrs = abbreviation for hours
67
('hours',[_('hrs.'),'hrs','hr','h'] + [ngettext('hour','hours',n) for n in range(5)]),
68
('days',[ngettext('day','days',n) for n in range(5)]),
69
('years',[ngettext('year','years',n) for n in range(5)]),
70
#('decades',[ngettext('decade','decades',n) for n in range(5)]),
71
#('centuries',[ngettext('century','centuries',n) for n in range(5)]),
72
#('millenia',[ngettext('millenium','millenia',n) for n in range(5)]),
76
if Converter.__single: raise Converter.__single
77
else: Converter.__single = self
78
self.create_conv_table()
79
self.create_density_table()
80
self.create_cross_unit_table()
81
self.create_vol_to_mass_table()
82
# right now we only track densities, but we might convert
83
# between other kinds of units eventually
84
self.cross_unit_dicts={'density':self.density_table}
85
## This allows for varied spellings of units to be entered.
86
self.create_unit_dict()
88
self.build_converter_dictionary()
89
self.build_converter_dictionary(self.v2m_table,density=True)
91
def add_time_table (self):
92
for u,conv in self.unit_to_seconds.items():
93
self.conv_table[(u,'seconds')]=conv
95
def create_conv_table(self):
96
self.conv_table = defaults.CONVERTER_TABLE.copy()
98
def create_density_table (self):
99
self.density_table = defaults.DENSITY_TABLE.copy()
101
def create_vol_to_mass_table (self):
102
self.v2m_table = defaults.VOL_TO_MASS_TABLE.copy()
104
def create_cross_unit_table (self):
105
self.cross_unit_table = defaults.CROSS_UNIT_TABLE.copy()
107
def create_unit_dict(self):
108
self.units = defaults.UNITS[0:]
109
for u,alts in self.time_units:
112
if not a in lst: lst.append(a)
113
if not a.title() in lst: lst.append(a.title())
114
if not a.capitalize() in lst: lst.append(a.capitalize())
115
if not a.upper() in lst: lst.append(a.upper())
116
if not a.lower() in lst: lst.append(a.lower())
117
self.units.append((u,lst))
118
self.unit_dict=PossiblyCaseInsensitiveDictionary()
119
for itm in self.units:
122
self.unit_dict[key] = key
124
self.unit_dict[v] = key
126
def build_converter_dictionary (self, table=None, density=False):
127
# first, make a list of all units in our dictionaries
129
convert = self.convert_simple
132
# Ignore anything that doesn't need density
133
if self.convert_simple(u1,u2): return None
134
return self.convert_w_density(u1,u2,density=1)
137
table=self.conv_table
139
#print "We were handed a table: ",table
140
for u1,u2 in filter(lambda x: len(x)==2, table.keys()):
141
if u1 not in units: units.append(u1)
142
if u2 not in units: units.append(u2)
143
#print 'done looping through list'
145
#print 'grabbing possible conversions for ',u
147
d=self.possible_conversions(u,dict=table)
149
# keep a list of what we've expanded
151
while len(to_expand) >= 1:
152
itm = to_expand.pop()
153
if itm not in expanded:
154
#debug('Expanding %s'%itm)
155
factor = convert(itm,u)
156
#debug('There are %s %s in a %s'%(factor,u,itm))
157
d2 = self.possible_conversions(itm)
159
for k,v in d2.items():
161
#debug('and there are %s %s in a %s'%(v,itm,k))
162
conversion = float(v) * float(factor)
163
# If we're doing density, we want to
164
# make sure we always have our tuples
166
if density and itm not in [key[0] for key in table.keys()]:
167
table[(u,k)]=float(1)/conversion
168
else: table[(k,u)]= conversion
169
if k not in expanded and k not in to_expand and k != itm and k != u:
173
def convert_simple (self, u1, u2, item=None):
178
if dict.has_key((u1,u2)):
180
elif dict.has_key((u2,u1)):
181
return float(1) / float(dict[(u2,u1)])
185
def convert_w_density (self, u1, u2, density=None, item=None):
189
if self.density_table.has_key(item):
190
density=self.density_table[item]
193
if self.v2m_table.has_key((u1,u2)):
194
conv = self.v2m_table[(u1,u2)]
195
return conv * density
196
elif self.v2m_table.has_key((u2,u1)):
197
conv = float(1) / float(self.v2m_table[(u2,u1)])
198
return conv / density
202
def list_of_cu_tables (self, dictcu=None):
204
dictcu = self.cross_unit_table
205
values = dictcu.values()
212
def conv_dict_for_item (self, item=None, dictcu=None, mult=None):
213
"""item overrides mult"""
215
dictcu = self.cross_unit_table
216
# tbls = self.list_of_cu_tables(dictcu)
219
for itm in dictcu.items():
222
dct = self.cross_unit_dicts[v[0]]
224
if item and dct.has_key(item):
230
def converter (self, u1, u2, item=None, density=None):
231
## Just a front end to convert_fancy that looks up units
232
if self.unit_dict.has_key(u1):
233
unit1 = self.unit_dict[u1]
236
if self.unit_dict.has_key(u2):
237
unit2 = self.unit_dict[u2]
240
## provide some kind of item lookup?
241
return self.convert_fancy(unit1, unit2, item=item, density=density)
243
def convert_fancy (self, u1, u2, item=None, density=None):
244
simple = self.convert_simple(u1,u2,self.conv_table)
245
if simple: return simple
246
# otherwise, we need to grab use a density table
247
debug('using density data')
248
return self.convert_w_density(u1,u2,item=item,density=density)
250
def get_conversions (self, u, item=None, density=None):
253
dct = self.conv_table.copy()
255
dct.update(self.conv_dict_for_item(item))
257
dct.update(self.conv_dict_for_item(mult=density))
258
return self.possible_conversions(u, dct)
260
def get_all_conversions (self, u, item=None, density=None):
261
dict = self.get_conversions(u, item, density)
263
conversions = dict.keys()
264
lst = conversions[0:] # make another copy to chew up
267
if not itm in conversions:
268
conversions.append(itm)
269
if not itm in expanded:
270
d = self.get_conversions(u,itm,density)
275
def possible_conversions(self, u, dict=0):
276
"""Return a dictionary of everything that unit u can convert to
277
The keys are what it can convert to and the values are the conversion
282
entries = dict.items()
287
ret[i2] = float(1) / item[1]
289
ret[i1] = float(item[1])
292
def readability_score (self,amt,unit=None):
293
"""We rate the readability of a number and unit
295
This is rather advanced. We presume a few things -- such as that we
296
like integers and simple fractions 1/2 1/3 1/4 better than other things.
298
We also learn from the defaults module what the bounds are for readability
299
for each amount given.
302
## We like our numbers whole or in normal fractions
305
elif integerp(amt * 2):
307
elif integerp(amt * 4):
309
elif integerp(amt * 3):
311
elif integerp (amt * 8):
313
elif integerp (amt * 6):
315
elif integerp (amt * 5):
317
elif integerp (amt * 10):
319
## If it is not a normal fraction, readability takes a hit
322
## If it is exactly 1 it gets a bump:
323
if amt == 1: readability += 0.5
325
# if we are beyond the min or max for our group, we take
326
# a substantial readability hit (this is worse than anything
329
u = self.unit_dict[unit]
331
debug("KeyError looking up unit",1)
334
ugroup,n = defaults.unit_group_lookup[u]
336
debug('Key Error for %s in \nunit_group_lookup: %s'%(unit,defaults.unit_group_lookup),
341
u,rng = defaults.UNIT_GROUPS[ugroup][n]
343
debug('Readability range for %s = %s:%s'%(u,mn,mx),8)
344
if mn and amt and amt < mn:
346
# we add a penalty proportional to the undersizedness
347
if (mn-amt): readability += -(2 * (mn - amt))/amt
350
# now we get clever and add a penalty proportional to the oversizedness
351
if (amt-mx): readability += - ((amt - mx)/float(mx))*2
353
# otherwise, we'll make some assumptions about numbers
354
# we don't like things over a thousand and we really don't
355
# or under a hundredth
356
if amt > 1000: readability += -2
365
## And we like numbers between 1/8 and 4
366
#if 0.25 <= amt <= 4:
368
### Less than 1/10th is getting too small
371
## And we rather dislike numbers over 10
372
#elif 20 >= amt > 10:
373
# readability += -0.9
374
## And we really can't have numbers over 20
375
#elif 100 >= amt > 20:
377
## And we really, really, really can't have numbers over 100
378
#elif 1000 >= amt > 100:
384
def adjust_unit (self, amt, unit, item=None, favor_current_unit=True, preferred_unit_groups=[]):
386
"""Return the most readable equivalent of amount and unit for item ITM
389
unit - our current unit
390
item - the item (in case it makes a difference -- currently not implemented)
391
favor_current_item - a flag; if True (default) we give our current unit a slight
392
preference, to avoid changing the unit if unnecessary.
393
Here we do our best to provide readable units, so that the user is presented
394
with 1/2 cup rather than 8 tablespoons, for example.
396
if not amt: return amt,unit
398
u = self.unit_dict[unit]
399
ugroup,n = defaults.unit_group_lookup[u]
403
units=defaults.UNIT_GROUPS[ugroup]
404
if preferred_unit_groups:
405
if ugroup not in preferred_unit_groups:
406
for ug in preferred_unit_groups:
407
conv = self.converter(u,defaults.UNIT_GROUPS[ug][0][0])
409
units = defaults.UNIT_GROUPS[ug]
411
u = unit = defaults.UNIT_GROUPS[ug][0][0]
415
ret_readability = self.readability_score(amt,unit)
416
if favor_current_unit: ret_readability += 1
422
conv = self.converter(u,u2)
426
readability = self.readability_score(new_amt,u2)
427
debug('%s %s, Readability = %s'%(new_amt,u2,readability),6)
429
if readability > ret_readability:
431
elif readability == ret_readability and abs(n-n1) < ret_distance:
435
ret_distance = abs(n-n1)
437
ret_readability = readability
439
debug('adjust unit called with %s %s, returning %s %s (R:%s)'%(amt,unit,ret_amt,ret_unit,
442
return ret_amt,ret_unit
444
def use_reasonable_unit (self, amt1, u1, amt2, u2, conv):
445
"""Given the conversion factor and the amounts,
446
we're going to try to figure out which unit
447
is the most human-readable. conv is what converts
448
from amt1 into amt2. We return a list of
449
amt unit, where the amount is our total, and the unit
450
is our chosen unit."""
451
u1amt = amt1 + amt2 * (1 / float(conv))
452
u2amt = amt2 + amt1 * conv
453
if self.readability_score(u1amt) >= self.readability_score(u2amt):
458
def add_reasonably (self, a1, u1, a2, u2, item=None):
459
"""Return a list with amount and unit if conversion is possible.
462
# we give up if these aren't numbers
464
conv = self.converter(u1,u2, item)
466
## Don't bother with all the work if the conversion factor is 1
470
return self.use_reasonable_unit(a1, u1, a2, u2, conv)
474
def amt_string(self, amt, approx=0.01):
475
"""Given list of [amount unit], hand back a string
476
representing amount. We'll do our best to turn numbers back
477
into fractions here so that they will look easily readable.
479
We can also handle amounts handed to us as tuples (as ranges)!"""
482
if type(num)==tuple or type(num)==list:
483
nstring=float_to_frac(num[0],approx=approx).strip()
484
if len(num)>1 and num[1]:
486
nstring += float_to_frac(num[1],approx=approx).strip()
488
nstring = float_to_frac(num,approx=approx)
490
return "%s %s" %(nstring, un)
494
def timestring_to_seconds (self, timestring):
495
"""Take a timestring and parse it into seconds.
497
We assume numbers come before time units - surely this will
498
break some languages(?). We'll build the parameter in when the
501
Return 0 if timestring is unparsable
503
# Before we do our real work, parse times that look like this 0:30
504
# Note the following will be true
506
# 00:00:20 = 20 seconds
507
if re.match('^\d\d?:\d\d(:\d\d)?$',timestring):
508
times = [locale.atof(s) for s in timestring.split(':')]
513
return h*60*60 + m*60 + s
515
for match in NUMBER_FINDER.finditer(timestring):
516
if numbers: numbers[-1].append(match.start())
517
numbers.append([match.start(),match.end()])
518
if numbers: numbers[-1].append(None)
520
for num_start,num_end,section_end in numbers:
521
num = frac_to_float(timestring[num_start:num_end])
522
unit = timestring[num_end:section_end].strip()
523
if self.unit_dict.has_key(unit):
524
conv = self.converter(unit,'seconds')
529
def timestring_to_seconds_old (self, timestring):
530
"""Take a timestring and parse it into seconds.
532
This logic may be a little fragile for non-English languages.
534
words = re.split('[ \s,;]+',str(timestring))
537
for n,w in enumerate(words):
538
if NUMBER_MATCHER.match(w): num.append(w)
539
elif num and self.unit_dict.has_key(w):
540
conv = self.converter(w,'seconds')
542
n = frac_to_float(" ".join(num))
543
if n: seconds += n * conv
545
if seconds: return seconds
547
def get_converter ():
553
# Each of our time formatting functions takes two arguments, which
554
# allows us to handle fractions in the outside world
556
#'millennia':lambda decades: ngettext("millenium","millenia",round(decades))
557
#'centuries':lambda decades: ngettext("century","centuries",round(decades))
558
#'decades':lambda decades: ngettext("decade","decades",round(decades))
559
'years':lambda years: ngettext("year","years",years),
560
'months':lambda months: ngettext("month","months",months),
561
'weeks':lambda weeks: ngettext("week","weeks",weeks),
562
'days':lambda days: ngettext("day","days",days),
563
'hours':lambda hours: ngettext("hour","hours",hours),
564
'minutes':lambda minutes: ngettext("minute","minutes",minutes),
565
'seconds':lambda seconds: ngettext("second","seconds",seconds),
568
def seconds_to_timestring (time, round_at=None, fractions=FRACTIONS_NORMAL):
571
units = Converter.unit_to_seconds.items()
572
units.sort(lambda a,b: a[1]<b[1] and 1 or a[1]>b[1] and -1 or 0)
573
for unit,divisor in units:
574
time_covered = time / int(divisor)
575
# special case hours, which we English speakers anyway are
576
# used to hearing in 1/2s -- i.e. 1/2 hour is better than 30
579
if time % divisor == .5*divisor and not time_strings:
582
if time_covered or add_half:
583
#print time_covered,'(rounds to ',round(time_covered),')'
584
if round_at and len(time_strings)+1>=round_at:
585
if not add_half: time_covered = int(round(float(time)/divisor))
586
time_strings.append(" ".join([
587
float_to_frac(time_covered,fractions=fractions),
588
# round because 1/2 = 1 as far as plural formas are concerned
589
time_formatters[unit](round(time_covered))
594
time_strings.append(" ".join([
595
float_to_frac(time_covered,fractions=fractions),
596
# round because 1/2 = 1 as far as plural forms are concerned
597
time_formatters[unit](round(time_covered))
599
time = time - time_covered * divisor
601
if len(time_strings)>2:
602
# Translators... this is a messay way of concatenating
603
# lists. In English we do lists this way: 1, 2, 3, 4, 5
604
# and 6. This set-up allows for variations of this system only.
605
# You can of course make your language only use commas or
606
# ands or spaces or whatever you like by translating both
607
# ", " and " and " with the same string.
608
return _(" and ").join([_(", ").join(time_strings[0:-1]),time_strings[-1]])
610
return _(" ").join(time_strings)
612
def integerp (num, approx=0.01):
613
"""approx can be a decimal that is a guide to rounding.
614
That is, if approx is 0.001, then we will consider
615
0.9991 and 1.0009 both to be the integer 1. This feature
616
will only work for positive, non-zero numbers."""
618
if int(num) == float(num):
621
bigside = int(num+approx)
622
smallside = int(num-approx)
623
if bigside != smallside:
631
# Fractions used in most charsets -- for certain exports, etc., we
632
# want to limit special fractions to these and use straight-up ascii
633
# fractions otherwise. These fractions must also be keys in
637
if hasattr(defaults,'NUMBERS'):
638
for n,words in defaults.NUMBERS.items():
641
all_number_words = NUMBER_WORDS.keys()
642
all_number_words.sort(
643
lambda x,y: ((len(y)>len(x) and 1) or (len(x)>len(y) and -1) or 0)
646
NUMBER_WORD_REGEXP = '|'.join(all_number_words).replace(' ','\s+')
647
FRACTION_WORD_REGEXP = '|'.join(filter(lambda n: NUMBER_WORDS[n]<1.0,
651
NORMAL_FRACTIONS = [(1,2),(1,4),(3,4)]
671
UNICODE_FRACTIONS = {
672
# a dictionary of funky unicode numbers not recognized by standard
673
# python float() and int() functions
691
SUP_DICT = {1:u'\u00B9',
697
SUB_DICT = {1:u'\u2081',
708
# nonstandard integers (sub or sup) that may be used in fractions
709
UNICODE_INTEGERS = {}
710
for d in SUB_DICT,SUP_DICT:
711
for k,v in d.items():
712
UNICODE_INTEGERS[v]=k
714
NUMBER_REGEXP = "[\d.,"
715
#for k in UNICODE_INTEGERS.keys(): NUMBER_REGEXP+=k # COVERED by re.UNICODE
716
for k in UNICODE_FRACTIONS.keys(): NUMBER_REGEXP+=k
717
NUMBER_START_REGEXP = NUMBER_REGEXP + ']'
718
NUMBER_END_REGEXP = NUMBER_REGEXP + SLASH
719
NUMBER_END_NO_RANGE_REGEXP = NUMBER_END_REGEXP + " /]"
720
NUMBER_END_REGEXP += " /-"
721
NUMBER_END_REGEXP += "]"
722
NUMBER_REGEXP = "("+NUMBER_START_REGEXP+NUMBER_END_REGEXP+"*"
723
if NUMBER_WORD_REGEXP:
724
NUMBER_REGEXP = NUMBER_REGEXP + '|' + NUMBER_WORD_REGEXP + ')'
725
NUMBER_NO_RANGE_REGEXP = '(' + NUMBER_START_REGEXP + '+|' + NUMBER_WORD_REGEXP + ')'
727
NUMBER_REGEXP = NUMBER_REGEXP + ")"
728
NUMBER_NO_RANGE_REGEXP = NUMBER_START_REGEXP + '+'
729
NUMBER_MATCHER = re.compile("^%s$"%NUMBER_REGEXP,re.UNICODE)
731
UNICODE_FRACTION_REGEXP = "[" + "".join(UNICODE_FRACTIONS.keys()) + "]"
732
DIVIDEND_REGEXP = "[0-9" + "".join(SUP_DICT.values()) + "]+"
733
SLASH_REGEXP = "[/" + SLASH + "]"
734
SLASH_MATCHER = re.compile(SLASH_REGEXP)
735
DIVISOR_REGEXP = "[0-9" + "".join(SUB_DICT.values()) + "]+"
736
FRACTION_REGEXP = "(" + UNICODE_FRACTION_REGEXP + "|" + DIVIDEND_REGEXP + \
737
SLASH_REGEXP + DIVISOR_REGEXP + ")"
739
AND_REGEXP = "(\s+%s\s+|\s*[&+]\s*|\s+)"%_('and')
742
if NUMBER_WORD_REGEXP:
743
NUM_AND_FRACTION_REGEXP = "((?P<int>%s+|%s)%s)?(?P<frac>(%s|%s))"%(NUMBER_START_REGEXP,
751
NUM_AND_FRACTION_REGEXP = "((?P<int>%s)+\s+)?(?P<frac>%s)"%(NUMBER_START_REGEXP,FRACTION_REGEXP)
753
FRACTION_MATCHER = re.compile(NUM_AND_FRACTION_REGEXP,re.UNICODE)
755
NUMBER_FINDER_REGEXP = "(%(NUM_AND_FRACTION_REGEXP)s|%(NUMBER_NO_RANGE_REGEXP)s)(?=($| |[\W]))"%locals()
756
NUMBER_FINDER = re.compile(NUMBER_FINDER_REGEXP,re.UNICODE)
758
# Note: the order matters on this range regular expression in order
759
# for it to properly split things like 1 - to - 3, which really do
761
RANGE_REGEXP = '([ -]*%s[ -]*|\s*-\s*)'%_('to') # for 'to' used in a range, as in 3-4
762
RANGE_MATCHER = re.compile(RANGE_REGEXP[1:-1]) # no parens for this one
765
# We need a special matcher to match known units when they are more
766
# than one word. The assumption remains that units should be one word
767
# -- but if we already know about two word units, then we should
770
multi_word_units = []
771
for canonical_name,other_names in defaults.UNITS:
772
if ' ' in canonical_name: multi_word_units.append(canonical_name)
773
for n in other_names:
774
if ' ' in n: multi_word_units.append(n)
775
MULTI_WORD_UNIT_REGEXP = '(' + \
776
'|'.join([re.escape(unicode(u)) for u in multi_word_units]) \
780
# generic ingredient matcher. This is far from a good matcher -- it's
781
# used as a fallback to test for things that obviously look like
782
# ingredients (e.g. 1 cup milk) that get misparsed by other ingredient
783
# parsers. This is often necessary because formats like mealmaster and
784
# mastercook are rarely actually followed.
785
NUMBER_FINDER_REGEXP2 = NUMBER_FINDER_REGEXP.replace('int','int2').replace('frac','frac2')
788
ING_MATCHER_REGEXP = """
789
\s* # opening whitespace
791
%(NUMBER_FINDER_REGEXP)s # a number
792
\s* # Extra whitespace
793
(%(RANGE_REGEXP)s # a possible range delimiter
794
\s* #More extra whitespace
795
%(NUMBER_FINDER_REGEXP2)s)? # and more numbers
796
)? # and of course no number is possible
797
\s* # Whitespace between number and unit
798
(?P<unit>\s*(%(MULTI_WORD_UNIT_REGEXP)s|[\w.]+))?\s+ # a unit
799
(?P<item>.*?)$ # and the rest of our stuff...
801
ING_MATCHER_REGEXP = ING_MATCHER_REGEXP%locals()
803
print 'Failure with local vars...'
804
for s in ['NUMBER_FINDER_REGEXP',
805
'NUMBER_FINDER_REGEXP2',
807
'MULTI_WORD_UNIT_REGEXP',]:
808
try: print 'DOUBLE CHECK',s,'%%(%s)s'%s%locals()
810
print 'Failed with ',s,locals()[s]
813
ING_MATCHER = re.compile(ING_MATCHER_REGEXP,
814
re.VERBOSE|re.UNICODE)
816
ING_MATCHER_AMT_GROUP = 'amount'
817
ING_MATCHER_UNIT_GROUP = 'unit'
818
ING_MATCHER_ITEM_GROUP = 'item'
820
def convert_fractions_to_ascii (s):
821
"""Convert all unicode-like fractions in string S with their ASCII equivalents"""
822
for nums,uni in NUM_TO_FRACTIONS.items():
823
s=re.sub(uni,"%s/%s"%(nums[0],nums[1]),s)
824
for d in SUB_DICT,SUP_DICT:
825
for num,uni in d.items():
826
s=re.sub(uni,str(num),s)
827
s=re.sub(SLASH,'/',s)
830
def fractify (decimal, divisor, approx=0.01, fractions=FRACTIONS_NORMAL):
831
"""Return fraction equivalent of decimal using divisor
833
If we don't have a fit within our approximation, return the
834
fraction. Otherwise, return False.
836
dividend = integerp(decimal*divisor)
838
if fractions==FRACTIONS_ASCII:
839
return "%s/%s"%(dividend,divisor)
840
elif fractions==FRACTIONS_ALL:
841
# otherwise, we have to do nice unicode magic
842
if NUM_TO_FRACTIONS.has_key((dividend,divisor)):
843
return NUM_TO_FRACTIONS[(dividend,divisor)]
845
if SUP_DICT.has_key(dividend): dividend = SUP_DICT[dividend]
846
if SUB_DICT.has_key(divisor): divisor = SUB_DICT[divisor]
847
return '%s%s%s'%(dividend,SLASH,divisor)
848
else: # fractions==FRACTIONS_NORMAL
849
#fallback to "normal" fractions -- 1/4, 1/2, 3/4 are special
850
if (dividend,divisor) in NORMAL_FRACTIONS:
851
return NUM_TO_FRACTIONS[(dividend,divisor)]
853
return "%s/%s"%(dividend,divisor)
855
def float_to_frac (n, d=[2,3,4,5,6,8,10,16],approx=0.01,fractions=FRACTIONS_NORMAL):
856
"""Take a number -- or anything that can become a float --
857
and attempt to return a fraction with a denominator in the list `d'. We
858
approximate fractions to within approx. i.e. if approx=0.01, then 0.331=1/3"""
859
if USE_FRACTIONS == FRACTIONS_OFF:
860
return float_to_metric(n,approx)
870
if rem==0 or rem<approx:
878
f = fractify(rem,div,approx=approx,fractions=fractions)
880
return " ".join([i,f]).strip()
881
# use locale-specific metric formatting if fractions don't work
882
return float_to_metric(n,approx)
884
def float_to_metric(n, approx=0.01):
885
"""Returns a formatted string in metric format, using locale-specific formatting"""
886
decimals_to_preserve = int(round(math.log(float(1)/approx,10)))
887
if decimals_to_preserve > 0:
888
format_string = "%."+str(decimals_to_preserve)+"f"
892
if (n - int(n) < approx) or ((n - int(n) + approx) > 1):
895
return float_to_metric(n,approx*.01)
896
return locale.format("%i",int(rounded),True)
898
rounded = round(n,decimals_to_preserve)
900
return float_to_metric(n,approx*.01)
901
return locale.format("%."+str(decimals_to_preserve)+"f",rounded,True) # format(formatstring, number, use_thousands_separator)
903
return locale.format("%i",n,True)
905
def float_string (s):
906
"""Convert string to a float, assuming it is some sort of decimal number
908
locale.atof should handle this, but we wrote our own to be a bit more flexible.
909
Specifically, we assume most numbers are decimals since recipes calling for
910
thousands and thousands of things are rare.
911
Also, we recognize items outside of our locale, since e.g. American might well be
912
importing British recipes and viceversa.
914
if NUMBER_WORDS.has_key(s.lower()):
915
print 'We have key',s.lower()
916
return NUMBER_WORDS[s.lower()]
917
THOUSEP = locale.localeconv()['thousands_sep']
918
DECSEP = locale.localeconv()['decimal_point']
919
if s.count(',') > 1 and s.count('.') <= 1:
920
# if we have more than one comma and less than one .
921
# then we assume ,s are thousand-separators
924
elif s.count(',') <= 1 and s.count('.') > 1:
925
# if we have more than one . and less than one ,,
926
# then we assume . is the thousand-separators
930
# otherwise let's check if this actually looks like a thousands separator
931
# before trusting our locale
932
elif re.search('[0-9]+%s[0-9][0-9][0-9]'%re.escape(THOUSEP),s):
933
return locale.atof(s)
934
elif THOUSEP and s.find(THOUSEP)>-1:
935
# otherwise, perhaps our thousand separator is really a
936
# decimal separator (we're out of our locale...)
937
print 'Warning: assuming %s is a decimal point in %s'%(THOUSEP,s)
938
s = s.replace(DECSEP,'!!!')
939
s = s.replace(THOUSEP,DECSEP)
940
s = s.replace('!!!',THOUSEP) # and remove any commas for good measure
941
return locale.atof(s)
943
# otherwise just trust our locale float
944
return locale.atof(s)
946
def frac_to_float (s):
947
"""We assume fractions look like this (I )?N/D"""
948
if NUMBER_WORDS.has_key(s): return NUMBER_WORDS[s]
949
if hasattr(s,'lower') and NUMBER_WORDS.has_key(s.lower()):
950
return NUMBER_WORDS[s.lower()]
952
m=FRACTION_MATCHER.match(s)
955
frac = m.group('frac')
956
if i: i=float_string(i)
958
if UNICODE_FRACTIONS.has_key(frac):
959
return i+UNICODE_FRACTIONS[frac]
960
elif NUMBER_WORDS.has_key(frac):
961
return i+NUMBER_WORDS[frac]
963
n,d = SLASH_MATCHER.split(frac)
964
n = SUP_DICT.get(n,n)
965
d = SUB_DICT.get(d,d)
966
return float(i)+(float(n)/float(d))
969
return float_string(s)
973
#If this isn't a fraction, we're just using float
975
return float_string(s)
979
#class ConverterSingleton:
981
# __impl == Converter
986
if __name__ == '__main__' and False:
987
class InteractiveConverter:
989
self.c = get_converter()
990
self.options = {'Convert':self.converter,
992
'Adjust':self.adjuster,
997
def offer_options (self):
998
print 'Choose one of the following actions:'
999
for k in self.options.keys(): print k
1000
choice = raw_input('Type your choice: ')
1001
if self.options.has_key(choice):
1002
self.options[choice]()
1004
print "I'm afraid I didn't understand your choice!"
1005
self.return_to_continue()
1006
self.offer_options()
1008
def get_unit (self, prompt="Enter unit: "):
1009
u = raw_input(prompt).strip()
1010
if self.c.unit_dict.has_key(u):
1013
for u in self.c.unit_dict.keys():
1016
return self.get_unit(prompt)
1018
print u, 'Is not a unit I know about! Please try again.'
1019
print '(Type "list" for a list of valid units)'
1020
return self.get_unit(prompt)
1022
def get_amount (self, prompt="Enter amount: "):
1023
amt = frac_to_float(raw_input(prompt))
1025
print "Please enter an amount!"
1026
return self.get_amount(prompt)
1030
def converter (self):
1031
u1 = self.get_unit("Enter source unit: ")
1032
amt = self.get_amount("Enter source amount: ")
1033
u2 = self.get_unit("Enter target unit: ")
1034
conv = self.c.converter(u1,u2)
1035
print '%s %s = %s %s'%(amt,u1,conv*amt,u2)
1036
self.return_to_continue()
1038
def adjuster (self):
1039
u1 = self.get_unit('Original unit: ')
1040
a1 = self.get_amount('Original amount: ')
1041
a,u = self.c.adjust_unit(a1,u1)
1042
print 'Most readable unit = %s %s'%(a,u)
1044
def return_to_continue (self):
1045
print 'Enter return to continue: '
1049
u1 = self.get_unit('Enter unit 1: ')
1050
a1 = self.get_amount("Enter amount 1: ")
1051
u2 = self.get_unit('Enter unit 2: ')
1052
a2 = self.get_amount('Enter amount 2: ')
1053
result = self.c.add_reasonably(a1,u1,a2,u2)
1055
print "%s %s + %s %s = %s"%(u1,a1,u2,a2,result)
1057
print "I'm sorry, I couldn't add that together!"
1058
self.return_to_continue()
1065
#i=InteractiveConverter()