# This file is part of Sympathy for Data.
# Copyright (c) 2013, 2017, Combine Control Systems AB
#
# SYMPATHY FOR DATA COMMERCIAL LICENSE
# You should have received a link to the License with Sympathy for Data.
from sympathy.api import qt2 as qt_compat
from sympathy.api import node as synode
from sympathy.api import node_helper
from sympathy.api.nodeconfig import Port, Ports, Tag, Tags
from sympathy.api.exceptions import SyConfigurationError
QtWidgets = qt_compat.QtWidgets
def slice_base_parameters():
parameters = synode.parameters()
parameters.set_string(
'slice', label='Slice', value='[:]',
description=('Use standard Python syntax to define pattern for slice '
'operation, [start:stop:step]'))
parameters.set_integer(
'limit', label='Limit preview to', value=100,
editor=synode.editors.bounded_spinbox_editor(0, 10000, 1),
description='Specify the maximum number of rows in the preview table')
return parameters
_slice_doc = """
Briefly, the slice operation removes values in the input data outside the
index range between given `start` and `stop`. `Step` determines which value
to keep inside the range. 1: every value, 2: every other value, etc.
In more detail, slicing can be thought of as an operation which generates
an index list starting from `start`, taking steps of length `step` and
finishes at the last index <= `stop`.
The generated index is list then compared with the indices of the
values in the input data (the first value has index 0).
Values whose index is contained in the generated index list are
kept and the others are removed.
To handle an unknown number of values (normal case), the indices can also
be specified relative to the end (last index). "Count stop from the end" is
enabled by default for `stop`, and when enabled, the effective index is:
end - `stop`. "Count start from the end" is also available and can be
enabled to treat `start` in a similar fashion.
.. warning::
Compared to slicing in Python, this method has some notable changes:
- Using `start` > `stop` to reverse the output is not supported.
Consequently strides can not be negative. Should `start` be larger
than `stop`, the output will be empty.
- The range is inclusive for both `start` and `stop`, meaning that the
value that matches the `stop` index can also be included.
Examples
========
:input: `['a', 'b', 'c', 'd', 'e', 'f']`
Example 1:
:start: 1
:stop: 4
:step: 1
:index: `[1, 2, 3, 4]`
:output: `['b', 'c', 'd', 'e']`
Example 2:
:start: 0
:stop: 5
:step: 2
:index: `[0, 2, 4]`
:output: `['a', 'c', 'e']`
Example 3:
:start: 0
:stop: 0 (Count stop from the end)
:step: 1
:index: `[0, 1, 2, 3, 4, 5]`
:output: `['b', 'c', 'd', 'e', 'f']`
Example 4:
:start: 0
:stop: 1 (Count stop from the end)
:step: 1
:index: `[0, 1, 2, 3, 4, 5]`
:output: `['b', 'c', 'd', 'e']`
"""
def set_slice_parameters(parameters):
parameters.set_integer(
'start', value=0, label='Start',
editor=synode.editors.bounded_spinbox_editor(0, None, step=1),
description=(
'Start, '
'specifies the start index of the value range.'
))
parameters.set_boolean(
'decrement_start', value=False, label='Count start from the end',
description=(
'Count start from the end, '
'when enabled, start is counted backwards from the end '
'(last index).'
))
parameters.set_integer(
'stop', value=0, label='Stop',
editor=synode.editors.bounded_spinbox_editor(0, None, step=1),
description=(
'Stop, '
'specifies the stop index of the value range.'
))
parameters.set_boolean(
'decrement_stop', value=True, label='Count stop from the end',
description=(
'Count stop from the end, '
'when enabled, stop is counted backwards from the end '
'(last index).'
))
parameters.set_integer(
'step', value=1, label='Step',
editor=synode.editors.bounded_spinbox_editor(1, None, step=1),
description=(
'Step, '
'specifies the step length taken in the value range and '
'determines which values inside of the range to keep.'
))
def get_slice(start, stop, step, length, decrement_start, decrement_stop):
if step <= 0:
raise SyConfigurationError('Step must be greater than 0.')
if start < 0:
raise SyConfigurationError('Start can not be negative.')
if stop < 0:
raise SyConfigurationError('Stop can not be negative.')
max_index = max(length - 1, 0)
if decrement_start:
start = max_index - start
if decrement_stop:
stop = max_index - stop
if start > stop:
# Slice which will return an empty result.
start = 0
stop = 0
step = 1
else:
# Indices before 0 are treated as non-existant.
stop = max(stop + 1, 0)
start = max(start, 0)
return slice(start, stop, step)
[docs]
class SliceRowsTable(synode.Node):
__doc__ = f"""
Slice rows in Table.
Slicing is used to remove rows from the input.
{_slice_doc}
"""
author = "Erik der Hagopian"
name = 'Slice rows in Table'
nodeid = 'org.sysess.sympathy.slice.rows.table'
icon = 'select_table_rows.svg'
tags = Tags(Tag.DataProcessing.Select)
related = ['org.sysess.sympathy.slice.columns.table']
inputs = Ports([Port.Table('Input Table', name='data')])
outputs = Ports([Port.Custom('table', 'Output Table', name='data',
preview=True)])
parameters = synode.parameters()
set_slice_parameters(parameters)
def execute(self, ctx):
input_data = ctx.input['data']
output_data = ctx.output['data']
start = ctx.parameters['start'].value
stop = ctx.parameters['stop'].value
step = ctx.parameters['step'].value
decrement_start = ctx.parameters['decrement_start'].value
decrement_stop = ctx.parameters['decrement_stop'].value
length = input_data.number_of_rows()
index = get_slice(start, stop, step, length,
decrement_start, decrement_stop)
output_data.update(input_data[index])
output_data.set_name(input_data.get_name())
output_data.set_table_attributes(input_data.get_table_attributes())
[docs]
@node_helper.list_node_decorator(['data'], ['data'])
class SliceRowsTables(SliceRowsTable):
name = 'Slice rows in Tables'
nodeid = 'org.sysess.sympathy.slice.rows.tables'
[docs]
class SliceColumnsTable(synode.Node):
__doc__ = f"""
Slice columns in Table.
Slicing is used to remove columns from the input.
{_slice_doc}
"""
author = "Erik der Hagopian"
name = 'Slice columns in Table'
nodeid = 'org.sysess.sympathy.slice.columns.table'
icon = 'select_table_columns.svg'
tags = Tags(Tag.DataProcessing.Select)
related = ['org.sysess.sympathy.slice.rows.table']
inputs = Ports([Port.Table('Input Table', name='data')])
outputs = Ports([Port.Custom('table', 'Output Table', name='data',
preview=True)])
parameters = synode.parameters()
set_slice_parameters(parameters)
def execute(self, ctx):
input_data = ctx.input['data']
output_data = ctx.output['data']
start = ctx.parameters['start'].value
stop = ctx.parameters['stop'].value
step = ctx.parameters['step'].value
decrement_start = ctx.parameters['decrement_start'].value
decrement_stop = ctx.parameters['decrement_stop'].value
length = input_data.number_of_columns()
index = get_slice(start, stop, step, length,
decrement_start, decrement_stop)
output_data.update(input_data[:, index])
output_data.set_name(input_data.get_name())
output_data.set_table_attributes(input_data.get_table_attributes())
[docs]
@node_helper.list_node_decorator(['data'], ['data'])
class SliceColumnsTables(SliceColumnsTable):
name = 'Slice columns in Tables'
nodeid = 'org.sysess.sympathy.slice.columns.tables'
[docs]
class SliceItemsList(synode.Node):
__doc__ = f"""
Slice List.
Slicing is used to remove items from the input list.
{_slice_doc}
"""
author = "Erik der Hagopian"
name = 'Slice List'
nodeid = 'org.sysess.sympathy.slice.list'
icon = 'slice_list.svg'
tags = Tags(Tag.DataProcessing.Select)
inputs = Ports([Port.Custom('[<a>]', 'Input List', name='data')])
outputs = Ports([Port.Custom('[<a>]', 'Output Table', name='data',
preview=True)])
parameters = synode.parameters()
set_slice_parameters(parameters)
def execute(self, ctx):
input_data = ctx.input['data']
output_data = ctx.output['data']
start = ctx.parameters['start'].value
stop = ctx.parameters['stop'].value
step = ctx.parameters['step'].value
decrement_start = ctx.parameters['decrement_start'].value
decrement_stop = ctx.parameters['decrement_stop'].value
length = len(input_data)
index = get_slice(start, stop, step, length,
decrement_start, decrement_stop)
for i in range(length)[index]:
output_data.append(input_data[i])