[
https://issues.apache.org/jira/browse/ARROW-8199?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Scott Wilson updated ARROW-8199:
--------------------------------
Attachment: ArrowCsv.cpp
Hi Wes,
I hope you and yours are staying healthy in this strange new world!
I've taken a stab at creating a DataFrame like cover for arrow::Table. My
first milestone was to see if I could come up with a df.eval() like
representation for single-line transforms -- see the EVAL2 macro. Attached
is my code, I'm not quite sure where, if anywhere, I should post it to get
your thoughts so I'm sending this email. (I posted an earlier version on
Jira Arrow-602.) Mainly I'd like to know if this looks like the direction
you're thinking for arrow::DataFrame?
Thanks, Scott
**** Code, also included as attachment
#include <cstdint>
#include <memory>
#include <numeric>
#include <string>
#include <iostream>
#include <optional>
#include <vector>
#include <xutility>
#include <arrow/api.h>
#include <arrow/filesystem/localfs.h>
#include <arrow/csv/api.h>
#include <arrow/result.h>
#include <arrow/builder.h>
#include <boost/iterator/iterator_facade.hpp>
#include <boost/range/iterator_range.hpp>
#include <boost/preprocessor.hpp>
using namespace std;
using namespace arrow;
// SBW 2020.04.15 For ArrayCoverRaw::iterator, we can simply use the the
pointer interface.
// Wes suggests returning std::optional<T>, but sizeof(double) <
sizeof(std::optional<double>) and
// is not a drop-in replacement for T, i.e. optional<T> can't be used in
expression, need optional<T>.value().
// STL container-like cover for arrow::Array.
// Only works for Array types that support raw_values().
template<typename ArrType>
class ArrayCoverRaw
{
public:
using T = typename ArrType::value_type;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
// Match size_type to Array offsets rather than using size_t and ptrdiff_t.
using size_type = int64_t;
using difference_type = int64_t;
using iterator = pointer;
using const_iterator = const_pointer;
using reverse_iterator = pointer;
using const_reverse_iterator = const_pointer;
ArrayCoverRaw(std::shared_ptr<ArrType>& array) : _array(array) {}
size_type size() const { return _array->length(); }
// Should non-const versions fail if Array is immutable?
iterator begin() { return const_cast<pointer>(_array->raw_values()); }
iterator end() { return
const_cast<pointer>(_array->raw_values()+_array->length()); }
reverse_iterator rbegin() { return
const_cast<pointer>(_array->raw_values()+_array->length()-1); }
reverse_iterator rend() { return
const_cast<pointer>(_array->raw_values()-1); }
const_iterator cbegin() const { return _array->raw_values(); }
const_iterator cend() const { return _array->raw_values()+_array->length();
}
const_reverse_iterator crbegin() const { return
_array->raw_values()+_array->length()-1; }
const_reverse_iterator crend() const { return _array->raw_values()-1; }
// We could return std::optional<T> to encapsulate IsNull() info, but this
would seem to break the expected semantics.
reference operator[](const difference_type off) {
assert(_array->data()->is_mutable()); return _array->raw_values()+off; }
const_reference operator[](const difference_type off) const { return
_array->raw_values()+off; }
// ISSUE: is there an interface for setting IsNull() if array is mutable.
bool IsNull(difference_type off) const { return _array->IsNull(off); }
protected:
std::shared_ptr<ArrType> _array;
};
// TODO: Add ArrayCoverString and iterators, perhaps others.
// Use template on RefType so we can create iterator and const_iterator by
changing Value.
// Use class specializations to support Arrays that don't have raw_values().
template <typename CType, typename RefType>
class ChunkedArrayIterator
: public boost::iterator_facade<ChunkedArrayIterator<CType, RefType>,
RefType, boost::random_access_traversal_tag>
{
public:
using difference_type = int64_t;
using T = CType;
using ArrayType = typename CTypeTraits<CType>::ArrayType;
using pointer = T*;
explicit ChunkedArrayIterator(std::shared_ptr<arrow::ChunkedArray> ch_arr =
0, difference_type pos = 0)
: _ch_arr(ch_arr)
{
set_position(pos);
}
bool IsNull() const
{
auto arr = _ch_arr->chunk(_chunk_index);
return arr->IsNull(_current-_first);
}
private:
friend class boost::iterator_core_access;
bool equal(ChunkedArrayIterator<CType, RefType> const& other) const
{
return this->_position == other._position;
}
void increment()
{
_position++;
// Need to move to next chunk?
if ((_current == _last) && ((_chunk_index+1) < _ch_arr->num_chunks()))
{
_chunk_index++;
auto arr = _ch_arr->chunk(_chunk_index);
auto typed_arr = std::static_pointer_cast<ArrayType>(arr);
_first = const_cast<pointer>(typed_arr->raw_values());
_last = _first + arr->length() - 1;
_current = _first;
}
else
{
_current++;
}
}
void decrement()
{
_position--;
// Need to move to previous chunk?
if ((_current == _first) && (_chunk_index > 0))
{
_chunk_index--;
auto arr = _ch_arr->chunk(_chunk_index);
auto typed_arr = std::static_pointer_cast<ArrayType>(arr);
_first = const_cast<pointer>(typed_arr->raw_values());
_last = _first + arr->length() - 1;
_current = _last;
}
else
{
_current--;
}
}
RefType& dereference() const { return *_current; }
void advance(difference_type n)
{
_position += n;
while (n > 0)
{
difference_type max_delta = _last - _current;
if ((max_delta >= n) || ((_chunk_index+1) == _ch_arr->num_chunks()))
{
_current += n;
return;
}
// Move to next chunk.
n -= max_delta;
_chunk_index++;
auto arr = _ch_arr->chunk(_chunk_index);
auto typed_arr = std::static_pointer_cast<ArrayType>(arr);
_first = const_cast<pointer>(typed_arr->raw_values());
_last = _first + arr->length() - 1;
_current = _first;
}
while (n < 0)
{
difference_type max_delta = _first - _current;
if ((max_delta <= n) || (_chunk_index == 0))
{
_current += n;
return;
}
// Move to previous chunk.
n -= max_delta;
_chunk_index--;
assert(_chunk_index >= 0);
auto arr = _ch_arr->chunk(_chunk_index);
auto typed_arr = std::static_pointer_cast<ArrayType>(arr);
_first = const_cast<pointer>(typed_arr->raw_values());
_last = _first + arr->length() - 1;
_current = _last;
}
}
difference_type distance_to(ChunkedArrayIterator<CType, RefType> const&
other)
{
return other._position - this->_position;
}
// Helper
void set_position(difference_type pos)
{
_position = pos;
const int nchunks = _ch_arr->num_chunks();
int64_t offset = 0;
for (_chunk_index = 0; _chunk_index < nchunks; _chunk_index++)
{
auto arr = _ch_arr->chunk(_chunk_index);
int64_t arr_rows = arr->length();
if (((offset+arr_rows) > pos) || ((_chunk_index+1)==nchunks))
{
auto typed_arr = std::static_pointer_cast<ArrayType>(arr);
_first = const_cast<T*>(typed_arr->raw_values());
_last = _first + arr_rows - 1;
_current = _first + (pos-offset);
return;
}
offset += arr_rows;
}
assert(false);
}
std::shared_ptr<arrow::ChunkedArray> _ch_arr;
// Which Array we're looking at.
int _chunk_index = 0;
// Pointers into current Array. Use first/last rather than begin/end for
symmetry of moving forward/backward.
pointer _first = 0;
pointer _current = 0;
pointer _last = 0;
// Cache position across all chunks for support of random access.
difference_type _position = 0;
};
// This implementation is a subclass for Arrays that use GetView(i),
GetString(i), etc.
// Concrete subclass only needs to implement dereference(i).
template <typename CType>
class ChunkedArrayIteratorIndexImpl
{
public:
using difference_type = int64_t;
using ArrayType = typename CTypeTraits<CType>::ArrayType;
explicit ChunkedArrayIteratorIndexImpl(std::shared_ptr<arrow::ChunkedArray>
ch_arr = 0, difference_type pos = 0)
: _ch_arr(ch_arr)
{
set_position(pos);
}
bool IsNull() const
{
auto arr = _ch_arr->chunk(_chunk_index);
return arr->IsNull(_current);
}
protected:
friend class boost::iterator_core_access;
bool equal(ChunkedArrayIteratorIndexImpl<CType> const& other) const
{
return this->_position == other._position;
}
void increment()
{
_position++;
// Need to move to next chunk?
if ((_current == _last) && ((_chunk_index+1) < _ch_arr->num_chunks()))
{
_chunk_index++;
auto arr = _ch_arr->chunk(_chunk_index);
_typed_arr = std::static_pointer_cast<ArrayType>(arr);
_last = arr->length() - 1;
_current = 0;
}
else
{
_current++;
}
}
void decrement()
{
_position--;
// Need to move to previous chunk?
if ((_current == _first) && (_chunk_index > 0))
{
_chunk_index--;
auto arr = _ch_arr->chunk(_chunk_index);
_typed_arr = std::static_pointer_cast<ArrayType>(arr);
_last = arr->length() - 1;
_current = _last;
}
else
{
_current--;
}
}
// RefType& dereference() const { return *_current; }
void advance(difference_type n)
{
_position += n;
while (n > 0)
{
difference_type max_delta = _last - _current;
if ((max_delta >= n) || ((_chunk_index+1) == _ch_arr->num_chunks()))
{
_current += n;
return;
}
// Move to next chunk.
n -= max_delta;
_chunk_index++;
auto arr = _ch_arr->chunk(_chunk_index);
_typed_arr = std::static_pointer_cast<ArrayType>(arr);
_last = arr->length() - 1;
_current = 0;
}
while (n < 0)
{
difference_type max_delta = 0 - _current;
if ((max_delta <= n) || (_chunk_index == 0))
{
_current += n;
return;
}
// Move to previous chunk.
n -= max_delta;
_chunk_index--;
auto arr = _ch_arr->chunk(_chunk_index);
_typed_arr = std::static_pointer_cast<ArrayType>(arr);
_last = arr->length() - 1;
_current = _last;
}
}
difference_type distance_to(ChunkedArrayIteratorIndexImpl<CType> const&
other)
{
return other._position - this->_position;
}
// Helper
void set_position(difference_type pos)
{
_position = pos;
const int nchunks = _ch_arr->num_chunks();
int64_t offset = 0;
for (_chunk_index = 0; _chunk_index < nchunks; _chunk_index++)
{
auto arr = _ch_arr->chunk(_chunk_index);
int64_t arr_rows = arr->length();
if (((offset+arr_rows) > pos) || ((_chunk_index+1)==nchunks))
{
_typed_arr = std::static_pointer_cast<ArrayType>(arr);
_last = arr_rows - 1;
_current = (pos-offset);
return;
}
offset += arr_rows;
}
assert(false);
}
std::shared_ptr<arrow::ChunkedArray> _ch_arr;
// Which Array we're looking at.
int _chunk_index = 0;
// Current Array. Use first/last rather than begin/end for symmetry of
moving forward/backward.
std::shared_ptr<ArrayType> _typed_arr;
difference_type _current = 0;
difference_type _last = 0;
// Cache position across all chunks for support of random access.
difference_type _position = 0;
};
// SBW 2020.04.23 for EVAL2() macro, even though code not called, need lhs
iterator for unused code branch to compile.
using ConstRefString = std::string;
template<>
class ChunkedArrayIterator<std::string, ConstRefString> :
public ChunkedArrayIteratorIndexImpl<std::string>,
public boost::iterator_facade<ChunkedArrayIterator<std::string,
ConstRefString>, ConstRefString, boost::random_access_traversal_tag>
{
public:
using difference_type = int64_t;
using RefType = ConstRefString;
explicit ChunkedArrayIterator(std::shared_ptr<arrow::ChunkedArray> ch_arr =
0, difference_type pos = 0)
: ChunkedArrayIteratorIndexImpl(ch_arr, pos)
{
}
// Cache value to avoid returning pointer to temp.
RefType& dereference() const { _cached =
_typed_arr->GetString(_current); return _cached; }
private:
mutable std::string _cached;
};
using ChunkedArrayIteratorString = ChunkedArrayIterator<std::string,
ConstRefString>;
template<>
class ChunkedArrayIterator<bool, const bool> :
public ChunkedArrayIteratorIndexImpl<bool>,
public boost::iterator_facade<ChunkedArrayIterator<bool, const bool>, const
bool, boost::random_access_traversal_tag>
{
public:
using difference_type = int64_t;
using RefType = const bool;
explicit ChunkedArrayIterator(std::shared_ptr<arrow::ChunkedArray> ch_arr =
0, difference_type pos = 0)
: ChunkedArrayIteratorIndexImpl(ch_arr, pos)
{
}
// Cache value to avoid returning pointer to temp.
RefType& dereference() const { _cached = _typed_arr->GetView(_current);
return _cached; }
private:
mutable bool _cached;
};
using ChunkedArrayIteratorBoolean = ChunkedArrayIterator<bool, const bool>;
// STL container-like cover for arrow::ChunkedArray.
// Only works for ChunkedArrays composed of Array types that support
raw_values().
template<typename CType>
class ChunkedArrayCover
{
public:
// Match size_type to Array offsets rather than using size_t and ptrdiff_t.
using size_type = int64_t;
using difference_type = int64_t;
using iterator = typename ChunkedArrayIterator<CType, CType>;
using const_iterator = typename ChunkedArrayIterator<CType, const CType>;
using reverse_iterator = iterator;
using const_reverse_iterator = const_iterator;
ChunkedArrayCover(std::shared_ptr<ChunkedArray>& array) : _array(array) {}
size_type size() const { return _array->length(); }
// Should non-const versions fail if Array is immutable?
iterator begin() { return iterator(_array); }
iterator end() { return iterator(_array, size()); }
reverse_iterator rbegin() { return iterator(_array, size()-1); }
reverse_iterator rend() { return iterator(_array, -1); }
const_iterator cbegin() const { return const_iterator(_array); }
const_iterator cend() const { return const_iterator(_array, size()); }
const_reverse_iterator crbegin() const { return const_iterator(_array,
size()-1); }
const_reverse_iterator crend() const { return const_iterator(_array, -1); }
protected:
std::shared_ptr<ChunkedArray> _array;
};
#if 0 // SBW 2020.04.23 No longer needed no that we're using ContRefString
= std::string.
template<>
class ChunkedArrayCover<std::string>
{
public:
using CType = std::string;
using size_type = int64_t;
using difference_type = int64_t;
// ISSUE: no specialization for ChunkedArrayIterator<std::string,
std::string>
// Not sure how to handle setting of std::string values since StringArray
doesn't provide LHS access.
using iterator = typename ChunkedArrayIterator<CType, const CType>;
using const_iterator = typename ChunkedArrayIterator<CType, const CType>;
using reverse_iterator = iterator;
using const_reverse_iterator = const_iterator;
ChunkedArrayCover(std::shared_ptr<ChunkedArray>& array) : _array(array) {}
size_type size() const { return _array->length(); }
// Should non-const versions fail if Array is immutable?
iterator begin() { return iterator(_array); }
iterator end() { return iterator(_array, size()); }
reverse_iterator rbegin() { return iterator(_array, size()-1); }
reverse_iterator rend() { return iterator(_array, -1); }
const_iterator cbegin() const { return const_iterator(_array); }
const_iterator cend() const { return const_iterator(_array, size()); }
const_reverse_iterator crbegin() const { return const_iterator(_array,
size()-1); }
const_reverse_iterator crend() const { return const_iterator(_array, -1); }
protected:
std::shared_ptr<ChunkedArray> _array;
};
#endif
struct TestFrame
{
TestFrame(std::shared_ptr<arrow::Table> table = nullptr) : _table(table)
{
}
auto find_column(const char* name) { return _table->GetColumnByName(name); }
template<typename CType> typename ChunkedArrayCover<CType>::iterator
begin(const char* name)
{
auto col = _table->GetColumnByName(name);
assert(col != nullptr);
ChunkedArrayCover<CType> cover(col);
return cover.begin();
}
template<typename CType> typename ChunkedArrayCover<CType>::iterator
end(const char* name)
{
auto col = _table->GetColumnByName(name);
assert(col != nullptr);
ChunkedArrayCover<CType> cover(col);
return cover.end();
}
// Append to end if Index==-1.
template<typename CType> bool add_column(const char* name, int Index = -1)
{
vector<CType> values(_table->num_rows());
return add_column(name, values, Index);
}
template<typename CType> bool add_column(const char* name, const
vector<CType>& values, int Index = -1)
{
using Builder = typename CTypeTraits<CType>::BuilderType;
assert(values.size() == _table->num_rows());
Builder builder;
builder.Resize(values.size());
builder.AppendValues(values);
shared_ptr<arrow::Array> array;
arrow::Status st = builder.Finish(&array);
auto ch_arr = std::make_shared<ChunkedArray>(array);
auto field = arrow::field(name, builder.type());
// Watch for existing name and delete if necessary.
int icol = _table->schema()->GetFieldIndex(name);
if (icol >= 0)
{
std::shared_ptr<arrow::Table> out;
_table->RemoveColumn(icol, &out);
_table = out;
}
std::shared_ptr<arrow::Table> out;
if (Index < 0)
Index = _table->num_columns();
st = _table->AddColumn(Index, field, ch_arr, &out);
if (st.ok())
{
_table = out;
return true;
}
return false;
}
std::shared_ptr<arrow::Table> _table;
};
// Generalizing std::transform() to take any number of input iterators.
//
https://medium.com/@vgasparyan1995/generalizing-std-transform-8d2c41e1f958
// https://github.com/vgasparyan1995/transform
#include "F:\Dev\transform-master\transform.h"
// Use BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)
// Use BOOST_PP_SEQ_FOR_EACH_I twice, once to build list of input
iterators, once to build arg list for lambda.
// Use BOOST_PP_TUPLE_ELEM to pull values out of tuple.
#define DF_INPUT_ITER(r, data, i, elem) \
data.begin<BOOST_PP_TUPLE_ELEM(2, 0,
elem)>(BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(2, 1, elem))) \
, \
BOOST_PP_IF(i, , data.end<BOOST_PP_TUPLE_ELEM(2, 0,
elem)>(BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(2, 1, elem)))) \
BOOST_PP_COMMA_IF(BOOST_PP_NOT(i))
#define LAMBDA_INPUT(r, data, i, elem) \
BOOST_PP_COMMA_IF(i) \
auto BOOST_PP_TUPLE_ELEM(2, 1, elem)
// Variable args are input 2-tuples (type, name).
// Initially used BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__), but I think this
is clearer to add inputs as pp_sequence.
// Function signature: transform(in1.begin(), in1.end(), in2.begin() ...
inN.begin(), dest.begin(), lambda).
#define EVAL2(df, dest_tuple, func_body, input_seq) \
{ \
using dest_type = BOOST_PP_TUPLE_ELEM(2, 0, dest_tuple); \
const char* dest_name = BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(2, 1,
dest_tuple)); \
if (is_number_type<CTypeTraits<dest_type>::ArrowType>::value) \
{ \
if (df.find_column(dest_name) == nullptr) \
df.add_column<dest_type>(dest_name); \
my::transform( \
BOOST_PP_SEQ_FOR_EACH_I(DF_INPUT_ITER, df, input_seq) \
df.begin<dest_type>(dest_name) , \
[] (BOOST_PP_SEQ_FOR_EACH_I(LAMBDA_INPUT, df, input_seq)) func_body ); \
} \
else \
{ \
using Builder = typename CTypeTraits<dest_type>::BuilderType; \
vector<dest_type> dest(df._table->num_rows()); \
my::transform( \
BOOST_PP_SEQ_FOR_EACH_I(DF_INPUT_ITER, df, input_seq) \
dest.begin() , \
[] (BOOST_PP_SEQ_FOR_EACH_I(LAMBDA_INPUT, df, input_seq)) func_body ); \
df.add_column<dest_type>(dest_name, dest); \
} \
}
int main(int argc, char *argv[])
{
auto fs = make_shared<fs::LocalFileSystem>();
auto r_input = fs->OpenInputStream("c:/temp/_DatasetP14Seizures.csv");
auto pool = default_memory_pool();
auto read_options = arrow::csv::ReadOptions::Defaults();
auto parse_options = arrow::csv::ParseOptions::Defaults();
auto convert_options = arrow::csv::ConvertOptions::Defaults();
auto r_table_reader = csv::TableReader::Make(pool, r_input.ValueOrDie(),
read_options, parse_options, convert_options);
auto r_read = r_table_reader.ValueOrDie()->Read();
auto p_table = r_read.ValueOrDie();
PrettyPrintOptions options{0};
arrow::PrettyPrint(*p_table, options, &std::cout);
// Test covers and iterators.
const Table& tlb = *p_table;
const int64_t rows = tlb.num_rows();
const int cols = tlb.num_columns();
for (int c = 0; c < cols; c++)
{
auto f = tlb.field(c);
const string& name = f->name();
int type_id = f->type()->id();
auto ch_arr = tlb.column(c);
auto values_buffer = ch_arr->chunk(0)->data()->buffers[1];
cout << "is_mutable: " << values_buffer->is_mutable() << endl;
switch (type_id)
{
case Type::DOUBLE:
{
#if 0
using iterator = ChunkedArrayIteratorRaw<arrow::DoubleArray, double>;
iterator it(ch_arr, 2);
cout << it.IsNull() << endl;
boost::iterator_range<iterator> range(it-2, it+8);
for (double val : range)
cout << val << endl;
#else
using cover = ChunkedArrayCover<double>;
using iterator = typename cover::iterator;
using range = typename boost::iterator_range<iterator>;
cover cvr(ch_arr);
auto begin = cvr.begin();
auto end = cvr.end();
auto rbegin = cvr.rbegin();
auto rend = cvr.rend();
auto it = begin;
it += 2;
cout << "value_isnull: " << it.IsNull() << endl;
range rng(it-2, it+8);
for (double val : rng)
cout << val << endl;
#endif
}
break;
case Type::STRING:
{
using iterator = ChunkedArrayIteratorString;
iterator it(ch_arr, 2);
cout << "value_isnull: " << it.IsNull() << endl;
boost::iterator_range<iterator> range(it-2, it+8);
for (const std::string val : range)
cout << val << endl;
}
break;
case Type::BOOL:
{
using iterator = ChunkedArrayIteratorBoolean;
iterator it(ch_arr, 2);
cout << "value_isnull: " << it.IsNull() << endl;
boost::iterator_range<iterator> range(it-2, it+8);
for (bool val : range)
cout << val << endl;
}
break;
default:
break;
}
}
// 1 cout << is_number_type<CTypeTraits<double>::ArrowType>::value << endl;
// 1 cout << is_number_type<CTypeTraits<int>::ArrowType>::value << endl;
// 0 cout << is_number_type<CTypeTraits<bool>::ArrowType>::value << endl;
// 0 cout << is_number_type<CTypeTraits<std::string>::ArrowType>::value <<
endl;
// Testing code, to check templates.
if (false)
{
TestFrame df;
auto beg = df.begin<double>("foo");
EVAL2(df, (double, dest), { return foo*foo; }, ((double, foo)));
EVAL2(df, (double, dest), { return foo*foo; }, ((double,
foo))((std::string, name)));
}
if (true)
{
TestFrame df(p_table);
auto beg = df.begin<double>("Duration");
EVAL2(df, (double, LogDur), { return log10(Duration); }, ((double,
Duration)));
EVAL2(df, (std::string, Length), { return (LogDur>3) ? "long" : "short"; },
((double, LogDur)));
arrow::PrettyPrint(*df._table, options, &std::cout);
}
return 1;
}
--
Scott B. Wilson
Chairman and Chief Scientist
Persyst Development Corporation
420 Stevens Avenue, Suite 210
Solana Beach, CA 92075
> [C++] Guidance for creating multi-column sort on Table example?
> ---------------------------------------------------------------
>
> Key: ARROW-8199
> URL: https://issues.apache.org/jira/browse/ARROW-8199
> Project: Apache Arrow
> Issue Type: Wish
> Components: C++
> Affects Versions: 0.16.0
> Reporter: Scott Wilson
> Priority: Minor
> Labels: c++, newbie
> Attachments: ArrowCsv.cpp
>
>
> I'm just coming up to speed with Arrow and am noticing a dearth of examples
> ... maybe I can help here.
> I'd like to implement multi-column sorting for Tables and just want to ensure
> that I'm not duplicating existing work or proposing a bad design.
> My thought was to create a Table-specific version of SortToIndices() where
> you can specify the columns and sort order.
> Then I'd create Array "views" that use the Indices to remap from the original
> Array values to the values in sorted order. (Original data is not sorted, but
> could be as a second step.) I noticed some of the array list variants keep
> offsets, but didn't see anything that supports remapping per a list of
> indices, but this may just be my oversight?
> Thanks in advance, Scott
--
This message was sent by Atlassian Jira
(v8.3.4#803005)