-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathletter_based_decoder.py
More file actions
220 lines (189 loc) · 8.3 KB
/
letter_based_decoder.py
File metadata and controls
220 lines (189 loc) · 8.3 KB
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
from typing import Dict, List, Optional
from pydantic import BaseModel
from base_decoder import ISA51Decoder
from builder import ComponentBuilder
from data_types import (
ISA51Id, ISA51Component
)
from static_source import (
id_letters, VARIABLES,
USER_CHOICE_TYPE, VARIABLE_MODIFIERS, ACTIVE_FUNCTIONS, PASSIVE_FUNCTIONS,
FUNCTION_MODIFIERS
)
class UserChoiceMap(BaseModel):
"""
Contains the possible custom mappings
(or user choices) for each component
that needs to be decoded.
"""
variables: Dict[str, ISA51Component]
active_functions: Dict[str, ISA51Component]
passive_functions: Dict[str, ISA51Component]
function_modifiers: Dict[str, ISA51Component]
class LetterBasedDecoder(ISA51Decoder):
"""
Implementation of an ISA51 code decoder
based on reading letters and component mappings.
"""
cb = ComponentBuilder()
def __init__(self, user_choices_map: Optional[UserChoiceMap] = None):
"""
Initializes the decoder by assigning a user choice map
(UserChoiceMap) if provided. Otherwise, creates an empty one.
"""
if user_choices_map is None:
user_choices_map = UserChoiceMap(
variables={},
active_functions={},
passive_functions={},
function_modifiers={},
)
self.uc_map = user_choices_map
def decode_isa_51(self, code: str) -> List[ISA51Id]:
"""
Decodes an ISA51 code string and returns a list of ISA51Id objects.
It consists of several steps divided into helper methods:
1. Decode the Variable.
2. Decode the Variable Modifiers.
3. Decode Active, Passive Functions and their Modifiers.
4. Build the final 'full_name' and create the ISA51Id object.
"""
# 1. Decode the Variable and remove it from the code
variable, code = self._parse_variable(code)
# 2. Decode Variable Modifiers and remove them from the code
variable_modifiers, code = self._parse_variable_modifiers(code)
# 3. Decode Active/Passive Functions and Function Modifiers
active_function, passive_functions, function_modifiers = \
self._parse_functions(code)
# 4. Build the 'full_name' and create the ISA51Id object
isa51_id = self._build_isa51_id(
variable=variable,
variable_modifiers=variable_modifiers,
active_function=active_function,
passive_functions=passive_functions,
function_modifiers=function_modifiers
)
return [isa51_id]
def _parse_variable(self, code: str) -> (ISA51Component, str):
"""
Decodes the first letter (Variable) and removes that letter from the code.
Applies a custom mapping if the variable name is of type
'USER_CHOICE_TYPE'.
"""
# Decode the initial variable
variable_component = self.cb.build_component(VARIABLES, code[0])
# If the variable requires a user mapping, apply it
if USER_CHOICE_TYPE in variable_component.name:
if variable_component.letter in self.uc_map.variables:
variable_component = self.uc_map.variables[variable_component.letter]
# Remove the first letter from the code
code = code[1:]
return variable_component, code
def _parse_variable_modifiers(self, code: str) -> (List[ISA51Component], str):
"""
Decodes up to two consecutive Variable Modifiers (if they exist)
and removes those letters from the code.
"""
variable_modifiers = []
# Check if the first letter corresponds to a Variable Modifier
if code and code[0] in id_letters[VARIABLE_MODIFIERS]:
modifier_1 = self.cb.build_component(VARIABLE_MODIFIERS, code[0])
variable_modifiers.append(modifier_1)
code = code[1:]
# Check if the second letter corresponds to a Variable Modifier
if code and code[0] in id_letters[VARIABLE_MODIFIERS]:
modifier_2 = self.cb.build_component(VARIABLE_MODIFIERS, code[0])
variable_modifiers.append(modifier_2)
code = code[1:]
return variable_modifiers, code
def _parse_functions(
self, code: str
) -> (Optional[ISA51Component], Optional[List[ISA51Component]], Optional[List[ISA51Component]]):
"""
Decodes Active and Passive Functions, as well as their modifiers.
Returns the Active Function, the list of Passive Functions, and the list of
Function Modifiers.
"""
active_function = None
passive_functions = []
function_modifiers = []
# Flags to know if an active or passive function has been found
have_af = False
have_pf = False
for char in code:
# Check if the letter corresponds to an Active Function
if char in id_letters[ACTIVE_FUNCTIONS]:
af = self.cb.build_component(ACTIVE_FUNCTIONS, char)
# Apply user mapping if necessary
if USER_CHOICE_TYPE in af.name:
if af.letter in self.uc_map.active_functions:
af = self.uc_map.active_functions[af.letter]
active_function = af
have_af = True
# Check if the letter corresponds to a Passive Function
elif char in id_letters[PASSIVE_FUNCTIONS]:
pf = self.cb.build_component(PASSIVE_FUNCTIONS, char)
# Apply user mapping if necessary
if USER_CHOICE_TYPE in pf.name:
if pf.letter in self.uc_map.passive_functions:
pf = self.uc_map.passive_functions[pf.letter]
passive_functions.append(pf)
have_pf = True
# Check if the letter corresponds to a Function Modifier
elif (have_af or have_pf) and char in id_letters[FUNCTION_MODIFIERS]:
fm = self.cb.build_component(FUNCTION_MODIFIERS, char)
# Apply user mapping if necessary
if USER_CHOICE_TYPE in fm.name:
if fm.letter in self.uc_map.function_modifiers:
fm = self.uc_map.function_modifiers[fm.letter]
function_modifiers.append(fm)
# Return the tuple with what was found
# (Active Function, list of Passive Functions, list of Modifiers)
if not passive_functions:
passive_functions = None
if not function_modifiers:
function_modifiers = None
return active_function, passive_functions, function_modifiers
@staticmethod
def _build_isa51_id(
variable: ISA51Component,
variable_modifiers: Optional[List[ISA51Component]],
active_function: Optional[ISA51Component],
passive_functions: Optional[List[ISA51Component]],
function_modifiers: Optional[List[ISA51Component]]
) -> ISA51Id:
"""
Builds the ISA51Id object from the decoded components,
concatenating their names to form the full_name.
"""
full_name_parts = []
# Add the variable name
if variable.name:
full_name_parts.append(variable.name)
# Add the variable modifiers (if any)
if variable_modifiers:
for vm in variable_modifiers:
full_name_parts.append(vm.name)
# Add the passive functions (if any)
if passive_functions:
for pf in passive_functions:
full_name_parts.append(pf.name)
# Add the active function (if any)
if active_function:
full_name_parts.append(active_function.name)
# Add the function modifiers (if any)
if function_modifiers:
for fm in function_modifiers:
full_name_parts.append(fm.name)
# Join everything into a single string separated by spaces
full_name = " ".join(full_name_parts)
# Build and return the ISA51Id object
return ISA51Id(
full_name=full_name,
is_in_allowed_table=False,
variable=variable,
variable_modifiers=variable_modifiers if variable_modifiers else None,
active_function=active_function,
passive_functions=passive_functions,
function_modifiers=function_modifiers
)