-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmeasures.py
149 lines (126 loc) · 5.12 KB
/
measures.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import re
from typing import Optional
class Region:
USA = 'usa'
EUROPE = 'eur'
class Measure:
"""
Measures in ml
"""
OUNCE_UK = 28.4
OUNCE_US = 29.6
PINT_UK = 568
PINT_US = 473 # 16 oz
class MeasureProcessor:
"""
MeasureProcessor: Convert a human-readable measure in a comment into ml
Complicated somewhat by the differing sizes of pints, ounces in the USA and the rest of the world.
So, we accept an input that says if we're in US or elsewhere (currently defined as "Europe")
"""
DEFAULT_SERVING_SIZES = {
Region.EUROPE: {
'draft': Measure.PINT_UK / 2, # Some personal preference here
'cask': Measure.PINT_UK / 2,
'taster': 150,
'bottle': 330,
'can': 330 # Starting to change towards 440
},
Region.USA: { # Ref: https://beerconnoisseur.com/articles/popular-beer-sizes
'draft': Measure.PINT_US,
'cask': Measure.PINT_US,
'taster': Measure.OUNCE_US * 4,
'bottle': Measure.OUNCE_US * 12,
'can': Measure.OUNCE_US * 12
}
}
MAX_VALID_MEASURE = 2500 # For detection of valid inputs. More than a yard of ale or a Maß
DEFAULT_UNIT = 'pint'
def __init__(self, region):
if region == Region.USA or region == Region.EUROPE:
self.region = region
else:
raise Exception("Region in measure processor must be Region.USA or Region.EUROPE")
self.units = {
'ml': 1,
'cl': 10,
'litre': 1000,
'liter': 1000, # Accept variant spelling
'sip': 25,
'taste': 25,
}
if region == Region.USA:
self.units['pint'] = Measure.PINT_US
self.units['ounce'] = Measure.OUNCE_US
self.units['oz'] = Measure.OUNCE_US
else:
self.units['pint'] = Measure.PINT_UK
self.units['ounce'] = Measure.OUNCE_UK
self.units['oz'] = Measure.OUNCE_UK
def parse_measure(self, measure_string: str) -> Optional[int]:
"""
Read a measure as recorded in the comment field and parse it into a number of millilitres
Args:
measure_string: String as found in square brackets
Returns:
Integer number of ml
"""
divisors = {'quarter': 4, 'third': 3, 'half': 2}
divisor_match = '(?P<divisor_text>' + '|'.join(divisors.keys()) + ')'
unit_match = '(?P<unit>' + '|'.join(self.units.keys()) + ')s?' # allow plurals
optional_unit_match = '(' + unit_match + ')?'
fraction_match = r'(?P<fraction>\d+/\d+)'
quantity_match = r'(?P<quantity>[\d\.]+)'
optional_space = r'\s*'
candidate_matches = [
'^' + unit_match + '$',
'^' + quantity_match + optional_space + optional_unit_match + '$',
'^' + divisor_match + optional_space + optional_unit_match + '$',
'^' + fraction_match + optional_space + optional_unit_match + '$',
]
match = None
quantity = None
for pattern in candidate_matches:
match = re.match(pattern, measure_string)
if match:
break
if match:
match_dict = match.groupdict()
unit = match_dict['unit'] if 'unit' in match_dict and match_dict['unit'] is not None else self.DEFAULT_UNIT
quantity = self.units[unit]
if 'quantity' in match_dict:
quantity *= float(match_dict['quantity'])
elif 'divisor_text' in match_dict:
quantity /= divisors[match_dict['divisor_text']]
elif 'fraction' in match_dict:
fraction_parts = [int(s) for s in match_dict['fraction'].split('/')]
quantity = quantity * fraction_parts[0] / fraction_parts[1]
if quantity > self.MAX_VALID_MEASURE:
raise Exception('Measure of [%s] appears to be invalid. Did you miss out a unit?' % measure_string)
return int(quantity) if quantity else None
def measure_from_serving(self, serving: str) -> Optional[int]:
"""
Get a default measure size from the serving style, if possible
Args:
serving: One of the Untappd serving names
Returns:
int measure in ml
"""
serving = serving.lower()
defaults = self.DEFAULT_SERVING_SIZES[self.region]
drink_measure = int(defaults[serving]) if serving in defaults else None
return drink_measure
def measure_from_comment(self, comment: str) -> Optional[int]:
"""
Extract any mention of a measure (in square brackets) from the comment string on a checkin, and parse it
Args:
comment: Checking comment field
Returns:
int measure in ml
"""
measure_match = re.search(r'\[([^\[\]]+)\]', comment) # evil thing to match!
match_string = measure_match[1] if measure_match else None
if match_string:
drink_measure = self.parse_measure(match_string)
else:
drink_measure = None
return drink_measure