Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle leading zeros in flatten/unflatten implementation #217

Merged
merged 7 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ All notable changes to this project will be documented in this file.

* ### Merged Pull Requests
* [211: Restructure the folder structure and add a basic code generator](https://github.com/ni/nidaqmx-python/pull/211)
* [217: Handle leading zeros in flatten/unflatten implementation](https://github.com/ni/nidaqmx-python/issues/217)
* ### Resolved Issues
* ...
* [216: Can read channel_names of PersistedTask but not channels](https://github.com/ni/nidaqmx-python/issues/216)
* ### Major Changes
* Refactored the repository folder structure and added a code generator.

Expand Down
56 changes: 46 additions & 10 deletions generated/nidaqmx/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ def flatten_channel_string(channel_names):
names to a single string prior to using the DAQmx Create Channel methods or
instantiating a DAQmx Task object.

Note: For simplicity, this implementation is not fully compatible with the
NI-DAQmx driver implementation, which is generally more permissive. For
example, the driver is more graceful with whitespace padding. It was deemed
valuable to implement this natively in Python, so it can be leveraged in
workflows that don't have the driver installed. If we have specific examples
where this approximation is a problem, we can revisit this in the future.

Args:
channel_names (List[str]): The list of physical or virtual channel
names.
Expand Down Expand Up @@ -52,13 +59,16 @@ def flatten_channel_string(channel_names):
previous = {
'base_name': channel_name,
'start_index': -1,
'end_index': -1
'start_index_str': "",
'end_index': -1,
'end_index_str': "",
}
else:
# If the channel name ends in a valid number, we may need to flatten
# this channel with subsequent channels in the x:y format.
current_base_name = m.group(1)
current_index = int(m.group(2))
current_index_str = m.group(2)
current_index = int(current_index_str)

if current_base_name == previous['base_name'] and (
(current_index == previous['end_index'] + 1 and
Expand All @@ -69,6 +79,7 @@ def flatten_channel_string(channel_names):
# previous and it's end index differs by 1, change the end
# index value. It gets flattened later.
previous['end_index'] = current_index
previous['end_index_str'] = current_index_str
else:
# If the current channel name has the same base name as the
# previous or it's end index differs by more than 1, it doesn't
Expand All @@ -78,7 +89,9 @@ def flatten_channel_string(channel_names):
previous = {
'base_name': current_base_name,
'start_index': current_index,
'end_index': current_index
'start_index_str': current_index_str,
'end_index': current_index,
'end_index_str': current_index_str,
}

# Convert the final channel dictionary to a flattened string
Expand All @@ -98,11 +111,11 @@ def _channel_info_to_flattened_name(channel_info):
return channel_info['base_name']
elif channel_info['start_index'] == channel_info['end_index']:
return '{0}{1}'.format(channel_info['base_name'],
channel_info['start_index'])
channel_info['start_index_str'])
else:
return '{0}{1}:{2}'.format(channel_info['base_name'],
channel_info['start_index'],
channel_info['end_index'])
channel_info['start_index_str'],
channel_info['end_index_str'])


def unflatten_channel_string(channel_names):
Expand All @@ -113,6 +126,13 @@ def unflatten_channel_string(channel_names):
physical or virtual channels into a list of physical or virtual channel
names.

Note: For simplicity, this implementation is not fully compatible with the
NI-DAQmx driver implementation, which is generally more permissive. For
example, the driver is more graceful with whitespace padding. It was deemed
valuable to implement this natively in Python, so it can be leveraged in
workflows that don't have the driver installed. If we have specific examples
where this approximation is a problem, we can revisit this in the future.

Args:
channel_names (str): The list or range of physical or virtual channels.

Expand Down Expand Up @@ -147,8 +167,18 @@ def unflatten_channel_string(channel_names):
raise DaqError(_invalid_range_syntax_message,
error_code=-200498)

num_before = int(m_before.group(2))
num_after = int(m_after.group(2))
num_before_str = m_before.group(2)
num_before = int(num_before_str)
num_after_str = m_after.group(2)
num_after = int(num_after_str)

num_min_width = 0
# If there are any leading 0s in the first number, we want to ensure
# match that width. This is established precedence in the DAQmx
# algorithm.
if num_before > 0 and len(num_before_str.lstrip('0')) < len(num_before_str):
num_min_width = len(num_before_str)

num_max = max([num_before, num_after])
num_min = min([num_before, num_after])
number_of_channels = (num_max - num_min) + 1
Expand All @@ -159,8 +189,14 @@ def unflatten_channel_string(channel_names):

colon_expanded_channel = []
for i in range(number_of_channels):
colon_expanded_channel.append(
'{0}{1}'.format(m_before.group(1), num_min + i))
current_number = num_min + i
if num_min_width > 0:
# Using fstrings to create format strings. Braces for days!
zero_padded_format_specifier = f"{{:0{num_min_width}d}}"
current_number_str = zero_padded_format_specifier.format(current_number)
else:
current_number_str = str(current_number)
colon_expanded_channel.append(f"{m_before.group(1)}{current_number_str}")

if num_after < num_before:
colon_expanded_channel.reverse()
Expand Down
56 changes: 46 additions & 10 deletions src/nidaqmx/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ def flatten_channel_string(channel_names):
names to a single string prior to using the DAQmx Create Channel methods or
instantiating a DAQmx Task object.

Note: For simplicity, this implementation is not fully compatible with the
zhindes marked this conversation as resolved.
Show resolved Hide resolved
NI-DAQmx driver implementation, which is generally more permissive. For
example, the driver is more graceful with whitespace padding. It was deemed
valuable to implement this natively in Python, so it can be leveraged in
workflows that don't have the driver installed. If we have specific examples
where this approximation is a problem, we can revisit this in the future.

Args:
channel_names (List[str]): The list of physical or virtual channel
names.
Expand Down Expand Up @@ -52,13 +59,16 @@ def flatten_channel_string(channel_names):
previous = {
'base_name': channel_name,
'start_index': -1,
'end_index': -1
'start_index_str': "",
'end_index': -1,
'end_index_str': "",
}
else:
# If the channel name ends in a valid number, we may need to flatten
# this channel with subsequent channels in the x:y format.
current_base_name = m.group(1)
current_index = int(m.group(2))
current_index_str = m.group(2)
current_index = int(current_index_str)

if current_base_name == previous['base_name'] and (
(current_index == previous['end_index'] + 1 and
Expand All @@ -69,6 +79,7 @@ def flatten_channel_string(channel_names):
# previous and it's end index differs by 1, change the end
# index value. It gets flattened later.
previous['end_index'] = current_index
previous['end_index_str'] = current_index_str
else:
# If the current channel name has the same base name as the
# previous or it's end index differs by more than 1, it doesn't
Expand All @@ -78,7 +89,9 @@ def flatten_channel_string(channel_names):
previous = {
'base_name': current_base_name,
'start_index': current_index,
'end_index': current_index
'start_index_str': current_index_str,
'end_index': current_index,
'end_index_str': current_index_str,
}

# Convert the final channel dictionary to a flattened string
Expand All @@ -98,11 +111,11 @@ def _channel_info_to_flattened_name(channel_info):
return channel_info['base_name']
elif channel_info['start_index'] == channel_info['end_index']:
return '{0}{1}'.format(channel_info['base_name'],
channel_info['start_index'])
channel_info['start_index_str'])
else:
return '{0}{1}:{2}'.format(channel_info['base_name'],
channel_info['start_index'],
channel_info['end_index'])
channel_info['start_index_str'],
channel_info['end_index_str'])


def unflatten_channel_string(channel_names):
Expand All @@ -113,6 +126,13 @@ def unflatten_channel_string(channel_names):
physical or virtual channels into a list of physical or virtual channel
names.

Note: For simplicity, this implementation is not fully compatible with the
NI-DAQmx driver implementation, which is generally more permissive. For
example, the driver is more graceful with whitespace padding. It was deemed
valuable to implement this natively in Python, so it can be leveraged in
workflows that don't have the driver installed. If we have specific examples
where this approximation is a problem, we can revisit this in the future.

Args:
channel_names (str): The list or range of physical or virtual channels.

Expand Down Expand Up @@ -147,8 +167,18 @@ def unflatten_channel_string(channel_names):
raise DaqError(_invalid_range_syntax_message,
error_code=-200498)

num_before = int(m_before.group(2))
num_after = int(m_after.group(2))
num_before_str = m_before.group(2)
num_before = int(num_before_str)
num_after_str = m_after.group(2)
num_after = int(num_after_str)

num_min_width = 0
# If there are any leading 0s in the first number, we want to ensure
# match that width. This is established precedence in the DAQmx
# algorithm.
if num_before > 0 and len(num_before_str.lstrip('0')) < len(num_before_str):
num_min_width = len(num_before_str)

num_max = max([num_before, num_after])
num_min = min([num_before, num_after])
number_of_channels = (num_max - num_min) + 1
Expand All @@ -159,8 +189,14 @@ def unflatten_channel_string(channel_names):

colon_expanded_channel = []
for i in range(number_of_channels):
colon_expanded_channel.append(
'{0}{1}'.format(m_before.group(1), num_min + i))
current_number = num_min + i
if num_min_width > 0:
# Using fstrings to create format strings. Braces for days!
zero_padded_format_specifier = f"{{:0{num_min_width}d}}"
current_number_str = zero_padded_format_specifier.format(current_number)
else:
current_number_str = str(current_number)
colon_expanded_channel.append(f"{m_before.group(1)}{current_number_str}")

if num_after < num_before:
colon_expanded_channel.reverse()
Expand Down
47 changes: 23 additions & 24 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,33 @@
import random

from nidaqmx.utils import flatten_channel_string, unflatten_channel_string
from nidaqmx.tests.fixtures import any_x_series_device
from nidaqmx.tests.helpers import generate_random_seed


class TestUtils(object):
"""
Contains a collection of pytest tests that validate the utilities
functionality in the NI-DAQmx Python API.
"""

@pytest.mark.parametrize('seed', [generate_random_seed()])
def test_flatten_channel_string(self, any_x_series_device, seed):
# Reset the pseudorandom number generator with seed.
random.seed(seed)

channels = ['Dev1/ai0', 'Dev1/ai1', 'Dev1/ai3', 'Dev2/ai0']
flattened_channels = 'Dev1/ai0:1,Dev1/ai3,Dev2/ai0'
assert flatten_channel_string(channels) == flattened_channels

assert flatten_channel_string([]) == ''

@pytest.mark.parametrize('seed', [generate_random_seed()])
def test_unflatten_channel_string(self, any_x_series_device, seed):
# Reset the pseudorandom number generator with seed.
random.seed(seed)

channels = ['Dev1/ai0', 'Dev1/ai1', 'Dev1/ai3', 'Dev2/ai0']
flattened_channels = 'Dev1/ai0:1,Dev1/ai3,Dev2/ai0'
assert unflatten_channel_string(flattened_channels) == channels

assert unflatten_channel_string('') == []
def test_basic_flatten_flatten_and_unflatten(self):
unflattened_channels = ['Dev1/ai0', 'Dev1/ai1', 'Dev1/ai2','Dev1/ai4', 'Dev2/ai0']
flattened_channels = 'Dev1/ai0:2,Dev1/ai4,Dev2/ai0'
assert flatten_channel_string(unflattened_channels) == flattened_channels
assert unflatten_channel_string(flattened_channels) == unflattened_channels

def test_backwards_flatten_flatten_and_unflatten(self):
unflattened_channels = ['Dev1/ai2', 'Dev1/ai1', 'Dev1/ai0', 'Dev1/ai4', 'Dev2/ai0']
flattened_channels = 'Dev1/ai2:0,Dev1/ai4,Dev2/ai0'
assert flatten_channel_string(unflattened_channels) == flattened_channels
assert unflatten_channel_string(flattened_channels) == unflattened_channels

def test_empty_flatten_flatten_and_unflatten(self):
unflattened_channels = []
flattened_channels = ''
assert flatten_channel_string(unflattened_channels) == flattened_channels
assert unflatten_channel_string(flattened_channels) == unflattened_channels

def test_leading_zeros_flatten_and_unflatten(self):
unflattened_channels = ["EV01", "EV02"]
flattened_channels = 'EV01:02'
assert flatten_channel_string(unflattened_channels) == flattened_channels
assert unflatten_channel_string(flattened_channels) == unflattened_channels