Source code for athlib.utils
"""General athlib utility functions"""
from .codes import PAT_THROWS, PAT_JUMPS, PAT_RELAYS, PAT_HURDLES, PAT_TRACK, \
PAT_LEADING_DIGITS, PAT_PERF, \
FIELD_EVENTS, MULTI_EVENTS, FIELD_SORT_ORDER
[docs]def normalize_gender(gender):
"Return M, F or raise a ValueError"
g = gender.upper()
if g:
g = g[0]
if g not in 'MF':
raise ValueError('cannot normalize gender = %s' % repr(gender))
return g
def str2num(s):
try:
return int(s)
except ValueError:
return float(s)
[docs]def parse_hms(t):
"""
Parse a time duration with 0, 1 or 2 colons and return seconds.
>>> from athlib.utils import parse_hms
>>> parse_hms('10')
10
>>> parse_hms('1:10')
70
>>> parse_hms('1:1:10')
3670
>>> parse_hms('1:1:10.1')
3670.1
>>> parse_hms(3670.1)
3670.1
"""
if isinstance(t, (float, int)):
return t
# Try : and ; separators
for sep in ':;':
if sep not in t:
continue
sec = 0
for s in t.split(sep):
sec *= 60
try:
sec += str2num(s)
except ValueError:
raise ValueError('cannot parse seconds from %s' % repr(t))
return sec
try:
return str2num(t)
except ValueError:
raise ValueError('cannot parse seconds from %s' % repr(t))
[docs]def get_distance(discipline):
"""
Return approx distance in metres, for sanity checking
:param discipline:
:return:
"""
# Ignore final words like ' road'
discipline = discipline.split()[0]
if discipline == "XC":
return None
elif discipline == 'MAR':
return 42195
elif discipline == "HM":
return 21098
elif discipline == "MILE":
return 1609
m = PAT_LEADING_DIGITS.match(discipline)
if not m:
return None
qty_text = m.group()
remains = discipline[len(qty_text):]
qty = float(qty_text)
if not remains:
return int(qty)
elif remains in ('m', 'mH', 'SC', 'h', 'H'):
return int(qty)
elif remains in ('k', 'K', 'km'):
return int(1000 * qty)
elif remains in ('M', 'Mi', 'MI'):
return int(1609 * qty)
def format_seconds_as_time(seconds, prec=0):
mins, secs = divmod(seconds, 60)
hours, mins = divmod(mins, 60)
frac = secs - int(secs)
if prec == 0:
frac = ''
elif prec == 1:
frac = ('%0.1f' % frac)[1:] # e.g.".3"
elif prec == 2:
frac = ('%0.2f' % frac)[1:] # e.g.".34"
elif prec == 3:
frac = ('%0.3f' % frac)[1:] # e.g.".342"
else:
raise ValueError("Precision must be 0, 1, 2 or 3 digits")
if hours:
t = "%d:%02d:%02d" % (hours, mins, secs)
elif mins:
t = "%d:%02d" % (mins, secs)
else:
t = "%d" % secs
return t + frac
[docs]def check_performance_for_discipline(discipline, textvalue):
"""
Fix up and return what they typed in, or raise ValueError
"""
# print "checkperf %s %s" % (discipline, repr(textvalue))
textvalue = textvalue.strip()
if discipline.lower() == "xc" and textvalue == "":
return textvalue
# fix up "," for the Frenchies
if "," in textvalue and "." not in textvalue:
textvalue = textvalue.replace(",", ".")
if ";" in textvalue:
textvalue = textvalue.replace(";", ':')
if not PAT_PERF.match(textvalue):
raise ValueError(
"Illegal numeric pattern. Use digits, ':' and '.' only")
if discipline in FIELD_EVENTS:
try:
distance = float(textvalue)
return "%0.2f" % distance
except ValueError:
raise ValueError(
"'%s' is not valid for length/height. Use "
"metres/centimetres e.g. '2.34'" % textvalue
)
elif discipline.upper() in MULTI_EVENTS:
try:
points = int(textvalue)
except ValueError:
raise ValueError(
"'%s' is not a valid points value for multi-events"
% textvalue)
if points < 500:
raise ValueError("Multi-events scores should be above 500")
if points > 9999:
raise ValueError("Multi-events scores should be below 10000")
return str(points)
else:
# It's a running distance. format check. Try to extract metres
distance = get_distance(discipline)
if textvalue.startswith("0:"):
textvalue = textvalue[2:]
if textvalue.startswith("00:"):
textvalue = textvalue[3:]
if distance and (distance <= 200) and (":" in textvalue) \
and ("." not in textvalue):
# print "fixing colon to stop "
textvalue = textvalue.replace(":", ".")
if discipline in ["800", "1500", "3000"]:
if "." not in textvalue:
chunks = textvalue.split(":")
if len(chunks) == 3:
textvalue = chunks[0] + ':' + chunks[1] + "." + chunks[2]
# we got hours/mins/secs, should have been min/sec +
# fraction
# Brain surgery for the idiots who think 2.33 is a valid 800m time
# if expect_minutes and (':' not in textvalue) and ('.' in textvalue):
# textvalue = textvalue.replace('.', ':')
# caught false positives
chunks = textvalue.split(":")
# The regex ensures we have 1, 2 or 3 chunks
if len(chunks) == 1:
hours = 0
minutes = 0
seconds = float(chunks[0])
elif len(chunks) == 2:
hours = 0
minutes = int(chunks[0])
seconds = float(chunks[1])
elif len(chunks) == 3:
hh, mm, ss = chunks
hours = int(hh)
minutes = int(mm)
seconds = float(ss)
if (minutes == 0) and (seconds >= 100):
raise ValueError(
"Please use mm:ss or h:mm:ss for times above 99 seconds")
if distance == 400 and minutes > 45:
"63:40 instead of 63.40"
seconds = minutes + 0.01 * seconds
hours = 0
minutes = 0
duration = 3600 * hours + 60 * minutes + seconds
# print "duration: %0.2f seconds" % duration
# do sanity checks. Over 11 metres per second is pretty fishy for a
# sprint
if distance and duration:
velocity = distance * 1.0 / duration
# print 'distance = %0.2d, duration = %d sec,
# velocity = %0.2f m/s' % (distance, duration, velocity)
if distance <= 400:
if velocity > 11.0:
raise ValueError(
"%s too fast for %s, check the format" %
(textvalue, discipline))
elif distance > 400:
if velocity > 10.0:
raise ValueError(
"%s too fast for %s, check the format" %
(textvalue, discipline))
if velocity < 0.5:
raise ValueError(
"%s too slow for %s, check the format" %
(textvalue, discipline))
else:
if discipline.upper() == 'XC':
if not minutes:
raise ValueError(
"Please use mm:ss for minutes and seconds, not mm.ss")
# Format consistently for output
if hours and minutes:
t = '%d:%02d:%05.2f' % (hours, minutes, seconds)
elif minutes:
t = '%d:%05.2f' % (minutes, seconds)
else:
t = '%0.2f' % seconds
# Strip trailing zeroes except for short ones
if len(t) > 4:
while t.endswith('0') and len(t) > 4:
t = t[0:-1]
if t.endswith('.'):
t = t[0:-1]
return t
[docs]def event_sort_key(event_name):
"""
Return a tuple which will sort into programme order
Track should be ordered by distance.
"""
if not event_name:
# Goes at the end
return 6, 0, "?"
m = PAT_THROWS.search(event_name)
if m:
order = FIELD_SORT_ORDER.index(event_name[0:2])
return 4, order, event_name
m = PAT_HURDLES.search(event_name)
if m:
distance = int(m.group(1))
return 2, distance, event_name
m = PAT_JUMPS.search(event_name)
if m:
order = FIELD_SORT_ORDER.index(event_name[0:2])
return 3, order, event_name
m = PAT_RELAYS.search(event_name)
if m:
distance = int(m.group(2))
return 5, distance, event_name
# track last, so '100' doesn't match before '100H'
m = PAT_TRACK.search(event_name)
if m:
distance = int(m.group(1))
return 1, distance, event_name
# anything else sorts to end
return 6, 0, event_name
[docs]def text_event_sort_key(event_name):
"Return a text version of the event_sort_key"
return "%d_%05d_%s" % event_sort_key(event_name)
[docs]def sort_by_discipline(stuff, attr="discipline"):
"Sort dicts or objects into the normal athletics order"
sorter = []
for thing in stuff:
if isinstance(thing, dict):
disc = thing.get(attr, None)
else:
# assume object
disc = getattr(thing, attr, None)
priority = event_sort_key(disc)
sorter.append((priority, thing))
sorter.sort()
return [thing for (priority, thing) in sorter]