forked from Kitware/ParaView
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnumber_list_parser.py
96 lines (80 loc) · 3.6 KB
/
number_list_parser.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
##########################################################################
#
# number_list_parser
#
# usage: parse_number_list("1, 2, 10 to 15, 20 to 30 by 2", int)
#
# will return: [1, 2, 10, 11, 12, 13, 14, 15, 20, 22, 24, 26, 28, 30]
#
# Currently only int is supported. To support float you must uncomment
# the frange method below and then test it :-)
class Error(Exception): pass
def parse_number_list(s, number_class):
"""Take a string representing a list of numbers and number ranges and return
a list of the parsed numbers. For example: '1, 2, 50 to 100 by 5'
Acceptable strings follow the grammar:
A =: <A> <B>
B =: <number> | <C>
C =: "<number> to <number>" <D>
D =: "by <number>" | ""
"""
# remove commas, convert to lower case, split into tokens and parse
tokens = s.replace(',', '').lower().split()
number_list = []
while len(tokens):
if tokens_are_range(tokens, number_class): number_list += pop_range_tokens(tokens, number_class)
else: number_list += [pop_number_token(number_class, tokens)]
return number_list
def is_number_class(token, number_class):
try: number_class(token)
except: return False
return True
def convert_to_number_class(token, number_class):
try: num = number_class(token)
except ValueError: raise Error("Expected number type '%s' not '%s'" % (number_class.__name__, token))
return num
def tokens_are_range(tokens, number_class):
"""Check if the given list of tokens appears to be a range of the form: 'X to Y'"""
# If there are less than two tokens or the second token is a number then
# these tokens do not appear to be a range
if len(tokens) < 2 or is_number_class(tokens[1], number_class): return False
return True
def pop_string_token(s, tokens):
if not len(tokens): tokens += " "
token = tokens.pop(0)
if token != s: raise Error("Expected '%s' not '%s'" % (s, token))
def pop_number_token(number_class, tokens):
if not len(tokens): tokens += " "
return convert_to_number_class(tokens.pop(0), number_class)
def pop_range_tokens(tokens, number_class):
"""Given a list of tokens try to parse them as 'X to Y' or 'X to Y by Z'.
If the parsing fails then raise a Error. All parsed tokens are
popped from the incoming tokens list."""
# Get first three tokens as "X" "to" "Y"
start = pop_number_token(number_class, tokens)
pop_string_token("to", tokens)
stop = pop_number_token(number_class, tokens)
# If there are no more tokens or the next token is a number then
# there is no "by Z", return the range
if not len(tokens) or is_number_class(tokens[0], number_class):
return build_number_range(number_class, start, stop)
# Get the next two tokens as "BY" "Z", return the range
pop_string_token("by", tokens)
step = pop_number_token(number_class, tokens)
return build_number_range(number_class, start, stop, step)
def build_number_range(number_class, start, stop, step=1):
if step == 0: raise Error, "Invalid BY value '0'"
if number_class is int:
if (start > stop and step > 0):
raise Error("Starting number '%s' > ending '%s'" % (start, stop))
if (start < stop and step < 0):
raise Error("Starting number '%s' < ending '%s' with negative step" % (start, stop))
return range(start, stop+cmp(step, 0), step)
else: raise Error("Unhandled number class '%s'" % number_class.__name__)
"""
import math
def frange(start, stop, step=1.0):
start = float(start)
count = int(math.ceil( (stop-start)/step ) )
return list(start + n*step for n in xrange(count))
"""