susumu.yata
null+****@clear*****
Fri Mar 7 17:42:47 JST 2014
susumu.yata 2014-03-07 17:42:47 +0900 (Fri, 07 Mar 2014) New Revision: a170efc8ed593afe922c2983acd7926789bd51af https://github.com/groonga/grnxx/commit/a170efc8ed593afe922c2983acd7926789bd51af Message: Implement reference column. Modified files: lib/grnxx/calc_impl.cpp lib/grnxx/calc_impl.hpp lib/grnxx/column.cpp lib/grnxx/column.hpp lib/grnxx/column_impl.cpp lib/grnxx/column_impl.hpp lib/grnxx/table.cpp lib/grnxx/table.hpp lib/grnxx/types.hpp test/test_grnxx.cpp Modified: lib/grnxx/calc_impl.cpp (+159 -15) =================================================================== --- lib/grnxx/calc_impl.cpp 2014-03-05 15:05:43 +0900 (826f82a) +++ lib/grnxx/calc_impl.cpp 2014-03-07 17:42:47 +0900 (d3a07dc) @@ -88,6 +88,10 @@ class ColumnNode : public CalcNode { // ノードを破棄する. ~ColumnNode() {} + ColumnImpl<T> *column() const { + return column_; + } + // 指定された値を返す. T get(Int64 i, RowID row_id) const { return column_->get(row_id); @@ -760,6 +764,76 @@ class ArithmeticNodeHelper<T, InvalidResult> { } }; +// 参照と対応するノード. +template <typename T> +class ReferenceNode : public OperatorNode<T> { + public: + using Result = T; + + // 指定されたノードに対する四則演算と対応するノードとして初期化する. + explicit ReferenceNode(CalcNode *lhs, CalcNode *rhs) + : OperatorNode<Result>(), + lhs_(static_cast<ColumnNode<Int64> *>(lhs)), + rhs_(static_cast<ColumnNode<T> *>(rhs)) {} + // ノードを破棄する. + ~ReferenceNode() {} + + // 行の一覧を受け取り,演算結果が真になる行のみを残して,残った行の数を返す. + Int64 filter(RowID *row_ids, Int64 num_row_ids); + + // 与えられた行の一覧について演算をおこない,その結果を取得できる状態にする. + void fill(const RowID *row_ids, Int64 num_row_ids); + + private: + ColumnNode<Int64> *lhs_; + ColumnNode<T> *rhs_; + std::vector<RowID> local_row_ids_; +}; + +// 行の一覧を受け取り,演算結果が真になる行のみを残して,残った行の数を返す. +template <typename T> +Int64 ReferenceNode<T>::filter(RowID *row_ids, Int64 num_row_ids) { + // 参照先が真になる行だけを残す. + lhs_->fill(row_ids, num_row_ids); + // FIXME: lhs が行 ID の一覧を持っているのにコピーしなければならない. + local_row_ids_.resize(num_row_ids); + for (Int64 i = 0; i < num_row_ids; ++i) { + local_row_ids_[i] = lhs_->get(i, row_ids[i]); + } + rhs_->fill(&*local_row_ids_.begin(), num_row_ids); + Int64 count = 0; + for (Int64 i = 0; i < num_row_ids; ++i) { + if (rhs_->get(i, local_row_ids_[i])) { + row_ids[count++] = row_ids[i]; + } + } + return count; +} + +// 行の一覧を受け取り,演算結果が真になる行のみを残して,残った行の数を返す. +template <> +Int64 ReferenceNode<String>::filter(RowID *row_ids, Int64 num_row_ids) { + // FIXME: String から Boolean の変換は未定義. + return 0; +} + +// 与えられた行の一覧について演算をおこない,その結果を取得できる状態にする. +template <typename T> +void ReferenceNode<T>::fill(const RowID *row_ids, Int64 num_row_ids) { + lhs_->fill(row_ids, num_row_ids); + // FIXME: lhs が行 ID の一覧を持っているのにコピーしなければならない. + local_row_ids_.resize(num_row_ids); + for (Int64 i = 0; i < num_row_ids; ++i) { + local_row_ids_[i] = lhs_->get(i, row_ids[i]); + } + // FIXME: わざわざコピーしなければならない. + rhs_->fill(&*local_row_ids_.begin(), num_row_ids); + this->data_.resize(num_row_ids); + for (Int64 i = 0; i < num_row_ids; ++i) { + this->data_[i] = rhs_->get(i, local_row_ids_[i]); + } +} + } // namespace // 指定された種類のノードとして初期化する. @@ -823,6 +897,9 @@ int CalcToken::get_binary_operator_priority(BinaryOperatorType operator_type) { case MODULUS_OPERATOR: { return 9; } + case REFERENCE_OPERATOR: { + return 10; + } } return 0; } @@ -1017,15 +1094,16 @@ bool CalcImpl::tokenize_query(const String &query, if (end == left.npos) { end = left.size(); } - // カラムもしくは Boolean, Int64, Float の定数に対応するノードを作成する. + // FIXME: アドホックに書き足したのでひどいことになっている. + // 最初に Boolean, Int64, Float の可能性を調べる. String token = left.prefix(end); - auto node = create_column_node(token); - if (!node) { - if (token == "TRUE") { - node = create_boolean_node(true); - } else if (token == "FALSE") { - node = create_boolean_node(false); - } else if (token.find_first_of('.') != token.npos) { + CalcNode *node = nullptr; + if (token == "TRUE") { + node = create_boolean_node(true); + } else if (token == "FALSE") { + node = create_boolean_node(false); + } else if (std::isdigit(static_cast<UInt8>(token[0]))) { + if (token.find_first_of('.') != token.npos) { node = create_float_node(static_cast<Float>(std::stod( std::string(reinterpret_cast<const char *>(token.data()), token.size())))); @@ -1034,10 +1112,46 @@ bool CalcImpl::tokenize_query(const String &query, std::string(reinterpret_cast<const char *>(token.data()), token.size())))); } - if (!node) { - return false; + } else { + // カラムと参照演算子に対応するノードを作成する. + // 参照演算子は単項演算子より優先順位が高いため,以降の処理における面倒を + // なくすべく,最後に作成したノードのみをトークン化する. + const Table *current_table = table_; + ColumnNode<Int64> *src_node = nullptr; + for ( ; ; ) { + auto delim_pos = token.find_first_of('.'); + auto column = current_table->get_column_by_name( + (delim_pos != token.npos) ? token.prefix(delim_pos) : token); + if (!column) { + return false; + } + node = create_column_node(column); + if (!node) { + return false; + } + if (src_node) { + node = create_binary_operator_node(REFERENCE_OPERATOR, + src_node, node); + if (!node) { + return false; + } + } + if (delim_pos == token.npos) { + // 参照演算子がなければここで終わる. + break; + } + + token = token.except_prefix(delim_pos + 1); + if (column->data_type() != INTEGER) { + return false; + } + src_node = static_cast<ColumnNode<Int64> *>(node); + current_table = src_node->column()->dest_table(); } } + if (!node) { + return false; + } tokens->push_back(CalcToken(node)); left = left.except_prefix(end); break; @@ -1168,11 +1282,7 @@ bool CalcImpl::push_token(const CalcToken &token, } // 指定された名前のカラムと対応するノードを作成する. -CalcNode *CalcImpl::create_column_node(const String &column_name) { - auto column = table_->get_column_by_name(column_name); - if (!column) { - return nullptr; - } +CalcNode *CalcImpl::create_column_node(Column *column) { std::unique_ptr<CalcNode> node; switch (column->data_type()) { case BOOLEAN: { @@ -1294,6 +1404,10 @@ CalcNode *CalcImpl::create_binary_operator_node( node.reset(create_arithmetic_node(binary_operator_type, lhs, rhs)); break; } + case REFERENCE_OPERATOR: { + node.reset(create_reference_node(lhs, rhs)); + break; + } } if (!node) { return nullptr; @@ -1681,4 +1795,34 @@ CalcNode *CalcImpl::create_arithmetic_node_4(CalcNode *lhs, CalcNode *rhs) { return nullptr; } +// 参照演算子と対応するノードを作成する. +CalcNode *CalcImpl::create_reference_node(CalcNode *lhs, CalcNode *rhs) { + // 左の被演算子は参照型のカラムでなければならない. + if ((lhs->data_type() != INTEGER) || + (lhs->type() != COLUMN_NODE) || + !static_cast<ColumnNode<Int64> *>(lhs)->column()->dest_table()) { + return nullptr; + } + // 右の被演算子もカラムでなければならない. + if (rhs->type() != COLUMN_NODE) { + return nullptr; + } + // 右の被演算子の型に応じたノードを作成する. + switch (rhs->data_type()) { + case BOOLEAN: { + return new ReferenceNode<Boolean>(lhs, rhs); + } + case INTEGER: { + return new ReferenceNode<Int64>(lhs, rhs); + } + case FLOAT: { + return new ReferenceNode<Float>(lhs, rhs); + } + case STRING: { + return new ReferenceNode<String>(lhs, rhs); + } + } + return nullptr; +} + } // namespace grnxx Modified: lib/grnxx/calc_impl.hpp (+7 -3) =================================================================== --- lib/grnxx/calc_impl.hpp 2014-03-05 15:05:43 +0900 (e71e929) +++ lib/grnxx/calc_impl.hpp 2014-03-07 17:42:47 +0900 (80390ae) @@ -24,7 +24,8 @@ enum BinaryOperatorType { MINUS_OPERATOR, MULTIPLIES_OPERATOR, DIVIDES_OPERATOR, - MODULUS_OPERATOR + MODULUS_OPERATOR, + REFERENCE_OPERATOR }; // 演算器を構成するノードの種類. @@ -167,8 +168,8 @@ class CalcImpl : public Calc { // トークンをひとつずつ解釈する. bool push_token(const CalcToken &token, std::vector<CalcToken> *stack); - // 指定された名前のカラムと対応するノードを作成する. - CalcNode *create_column_node(const String &column_name); + // 指定されたカラムと対応するノードを作成する. + CalcNode *create_column_node(Column *column); // 指定された Boolean の定数と対応するノードを作成する. CalcNode *create_boolean_node(Boolean value); @@ -226,6 +227,9 @@ class CalcImpl : public Calc { // T: 算術演算子. template <typename T> CalcNode *create_arithmetic_node_4(CalcNode *lhs, CalcNode *rhs); + + // 参照演算子と対応するノードを作成する. + CalcNode *create_reference_node(CalcNode *lhs, CalcNode *rhs); }; } // namespace grnxx Modified: lib/grnxx/column.cpp (+9 -1) =================================================================== --- lib/grnxx/column.cpp 2014-03-05 15:05:43 +0900 (a64ff4d) +++ lib/grnxx/column.cpp 2014-03-07 17:42:47 +0900 (086d204) @@ -76,7 +76,7 @@ std::ostream &operator<<(std::ostream &stream, const Column &column) { } // 指定されたカラムを作成して返す. -Column *ColumnHelper::create(Table *table, +Column *ColumnHelper::create_column(Table *table, ColumnID column_id, const String &column_name, DataType data_type) { @@ -97,4 +97,12 @@ Column *ColumnHelper::create(Table *table, return nullptr; } +// 指定された参照型のカラムを作成して返す. +Column *ColumnHelper::create_reference_column(Table *table, + ColumnID column_id, + const String &column_name, + Table *dest_table) { + return new ColumnImpl<Int64>(table, column_id, column_name, dest_table); +} + } // namespace grnxx Modified: lib/grnxx/column.hpp (+9 -4) =================================================================== --- lib/grnxx/column.hpp 2014-03-05 15:05:43 +0900 (66a0355) +++ lib/grnxx/column.hpp 2014-03-07 17:42:47 +0900 (04111e7) @@ -57,10 +57,15 @@ std::ostream &operator<<(std::ostream &stream, const Column &column); class ColumnHelper { public: // 指定されたカラムを作成して返す. - static Column *create(Table *table, - ColumnID column_id, - const String &column_name, - DataType data_type); + static Column *create_column(Table *table, + ColumnID column_id, + const String &column_name, + DataType data_type); + // 指定された参照型のカラムを作成して返す. + static Column *create_reference_column(Table *table, + ColumnID column_id, + const String &column_name, + Table *dest_table); }; } // namespace grnxx Modified: lib/grnxx/column_impl.cpp (+10 -1) =================================================================== --- lib/grnxx/column_impl.cpp 2014-03-05 15:05:43 +0900 (151edfe) +++ lib/grnxx/column_impl.cpp 2014-03-07 17:42:47 +0900 (d346e21) @@ -1,6 +1,7 @@ #include "grnxx/column_impl.hpp" #include "grnxx/index.hpp" +#include "grnxx/table.hpp" //#include <iostream> @@ -60,8 +61,10 @@ template class ColumnImpl<Float>; #ifdef GRNXX_ENABLE_VARIABLE_INTEGER_TYPE // カラムを初期化する. -ColumnImpl<Int64>::ColumnImpl(Table *table, ColumnID id, const String &name) +ColumnImpl<Int64>::ColumnImpl(Table *table, ColumnID id, const String &name, + Table *dest_table) : Column(table, id, name, INTEGER), + dest_table_(dest_table), data_8_(MIN_ROW_ID, 0), data_16_(), data_32_(), @@ -112,6 +115,12 @@ void ColumnImpl<Int64>::resize(RowID max_row_id) { // 指定された ID の値を更新する. void ColumnImpl<Int64>::set(RowID row_id, Int64 value) { + if (dest_table_) { + if ((value < dest_table_->min_row_id()) || + (value > dest_table_->max_row_id())) { + throw "invalid reference"; + } + } switch (internal_data_type_size_) { case 8: { if ((value < INT8_MIN) || (value > INT8_MAX)) { Modified: lib/grnxx/column_impl.hpp (+17 -1) =================================================================== --- lib/grnxx/column_impl.hpp 2014-03-05 15:05:43 +0900 (a0a3fc1) +++ lib/grnxx/column_impl.hpp 2014-03-07 17:42:47 +0900 (9100225) @@ -9,6 +9,8 @@ namespace grnxx { template <typename T> class ColumnImpl : public Column { public: + using Value = T; + // カラムを初期化する. ColumnImpl(Table *table, ColumnID id, const String &name); // カラムを破棄する. @@ -42,8 +44,11 @@ class ColumnImpl : public Column { template <> class ColumnImpl<Int64> : public Column { public: + using Value = Int64; + // カラムを初期化する. - ColumnImpl(Table *table, ColumnID id, const String &name); + ColumnImpl(Table *table, ColumnID id, const String &name, + Table *dest_table = nullptr); // カラムを破棄する. ~ColumnImpl(); @@ -59,6 +64,12 @@ class ColumnImpl<Int64> : public Column { // 指定された行 ID が使えるようにサイズを変更する. void resize(RowID max_row_id); + // 参照先のテーブルを返す. + // なければ nullptr を返す. + Table *dest_table() const { + return dest_table_; + } + // 指定された ID の値を返す. Int64 get(RowID row_id) const { switch (internal_data_type_size_) { @@ -80,6 +91,7 @@ class ColumnImpl<Int64> : public Column { void set(RowID row_id, Int64 value); private: + Table *dest_table_; std::vector<Int8> data_8_; std::vector<Int16> data_16_; std::vector<Int32> data_32_; @@ -94,6 +106,8 @@ class ColumnImpl<Int64> : public Column { template <> class ColumnImpl<Int64> : public Column { public: + using Value = Int64; + // カラムを初期化する. ColumnImpl(Table *table, ColumnID id, const String &name); // カラムを破棄する. @@ -127,6 +141,8 @@ class ColumnImpl<Int64> : public Column { template <> class ColumnImpl<String> : public Column { public: + using Value = String; + // カラムを初期化する. ColumnImpl(Table *table, ColumnID id, const String &name); // カラムを破棄する. Modified: lib/grnxx/table.cpp (+32 -1) =================================================================== --- lib/grnxx/table.cpp 2014-03-05 15:05:43 +0900 (a9303b4) +++ lib/grnxx/table.cpp 2014-03-07 17:42:47 +0900 (8b321ac) @@ -2,6 +2,7 @@ #include "grnxx/calc.hpp" #include "grnxx/column_impl.hpp" +#include "grnxx/database.hpp" #include "grnxx/index.hpp" #include "grnxx/sorter.hpp" @@ -86,7 +87,37 @@ Column *Table::create_column(const String &column_name, DataType data_type) { columns_.resize(column_id + 1); } std::unique_ptr<Column> new_column( - ColumnHelper::create(this, column_id, column_name, data_type)); + ColumnHelper::create_column(this, column_id, column_name, data_type)); + new_column->resize(max_row_id()); + columns_map_.insert(it, std::make_pair(new_column->name(), column_id)); + columns_[column_id] = std::move(new_column); + return columns_[column_id].get(); +} + +// 参照型のカラムを作成して返す. +// 失敗すると nullptr を返す. +Column *Table::create_reference_column(const String &column_name, + const String &table_name) { + Table *dest_table = database_->get_table_by_name(table_name); + if (!dest_table) { + return nullptr; + } + auto it = columns_map_.find(column_name); + if (it != columns_map_.end()) { + return nullptr; + } + ColumnID column_id = min_column_id(); + for ( ; column_id <= max_column_id(); ++column_id) { + if (!columns_[column_id]) { + break; + } + } + if (column_id > max_column_id()) { + columns_.resize(column_id + 1); + } + std::unique_ptr<Column> new_column( + ColumnHelper::create_reference_column(this, column_id, column_name, + dest_table)); new_column->resize(max_row_id()); columns_map_.insert(it, std::make_pair(new_column->name(), column_id)); columns_[column_id] = std::move(new_column); Modified: lib/grnxx/table.hpp (+4 -0) =================================================================== --- lib/grnxx/table.hpp 2014-03-05 15:05:43 +0900 (8b019cd) +++ lib/grnxx/table.hpp 2014-03-07 17:42:47 +0900 (8e51a8c) @@ -32,6 +32,10 @@ class Table { // 指定された名前とデータ型のカラムを作成して返す. // 失敗すると nullptr を返す. Column *create_column(const String &column_name, DataType data_type); + // 参照型のカラムを作成して返す. + // 失敗すると nullptr を返す. + Column *create_reference_column(const String &column_name, + const String &table_name); // 指定された名前のカラムを破棄する. // 成功すれば true を返し,失敗すれば false を返す. bool drop_column(const String &column_name); Modified: lib/grnxx/types.hpp (+1 -0) =================================================================== --- lib/grnxx/types.hpp 2014-03-05 15:05:43 +0900 (f8a1d78) +++ lib/grnxx/types.hpp 2014-03-07 17:42:47 +0900 (3069069) @@ -2,6 +2,7 @@ #define GRNXX_TYPES_HPP #include <algorithm> +#include <cctype> #include <cstdint> #include <cstring> #include <functional> Modified: test/test_grnxx.cpp (+147 -0) =================================================================== --- test/test_grnxx.cpp 2014-03-05 15:05:43 +0900 (1b8d0a6) +++ test/test_grnxx.cpp 2014-03-07 17:42:47 +0900 (b53abe4) @@ -579,6 +579,152 @@ void test_calc() { } } +void test_reference() { + grnxx::Database database; + + grnxx::Table *src_table = database.create_table("Src"); + assert(src_table); + grnxx::Table *dest_table = database.create_table("Dest"); + assert(dest_table); + + auto reference_column = dynamic_cast<grnxx::ColumnImpl<grnxx::Int64> *>( + src_table->create_reference_column("Reference", "Dest")); + assert(reference_column); + + auto boolean_column = dynamic_cast<grnxx::ColumnImpl<grnxx::Boolean> *>( + dest_table->create_column("Boolean", grnxx::BOOLEAN)); + assert(boolean_column); + auto integer_column = dynamic_cast<grnxx::ColumnImpl<grnxx::Int64> *>( + dest_table->create_column("Integer", grnxx::INTEGER)); + assert(integer_column); + auto float_column = dynamic_cast<grnxx::ColumnImpl<grnxx::Float> *>( + dest_table->create_column("Float", grnxx::FLOAT)); + assert(float_column); + + std::mt19937_64 random; + + std::vector<grnxx::Int64> boolean_data; + std::vector<grnxx::Int64> integer_data; + std::vector<grnxx::Int64> float_data; + for (grnxx::Int64 i = 0; i < 1000; ++i) { + boolean_data.push_back(random() & 1); + integer_data.push_back(random() % 100); + float_data.push_back(1.0 * random() / random.max()); + grnxx::RowID row_id = dest_table->insert_row(); + boolean_column->set(row_id, boolean_data[i]); + integer_column->set(row_id, integer_data[i]); + float_column->set(row_id, float_data[i]); + } + + std::vector<grnxx::Int64> reference_data; + for (grnxx::Int64 i = 0; i < 1000; ++i) { + grnxx::RowID row_id = src_table->insert_row(); + reference_data.push_back(random() % 1000); + reference_column->set(row_id, reference_data[i] + 1); + } + + std::vector<grnxx::RowID> all_row_ids(1000); + std::unique_ptr<grnxx::RowIDCursor> cursor(src_table->create_cursor()); + assert(cursor->get_next(&all_row_ids[0], 1000) == 1000); + + // Boolean で絞り込む. + { + std::unique_ptr<grnxx::Calc> calc( + src_table->create_calc("Reference.Boolean")); + assert(calc); + std::vector<grnxx::RowID> row_ids(all_row_ids); + grnxx::Int64 num_row_ids = calc->filter(&*row_ids.begin(), row_ids.size()); + assert(num_row_ids != 0); + grnxx::Int64 count = 0; + for (grnxx::Int64 i = 0; i < 1000; ++i) { + grnxx::RowID row_id = grnxx::MIN_ROW_ID + i; + if (boolean_data[reference_data[i]]) { + assert(row_ids[count] == row_id); + assert(++count <= num_row_ids); + } + } + assert(count == num_row_ids); + } + + // Boolean で絞り込む. + { + std::unique_ptr<grnxx::Calc> calc( + src_table->create_calc("!Reference.Boolean")); + assert(calc); + std::vector<grnxx::RowID> row_ids(all_row_ids); + grnxx::Int64 num_row_ids = calc->filter(&*row_ids.begin(), row_ids.size()); + assert(num_row_ids != 0); + grnxx::Int64 count = 0; + for (grnxx::Int64 i = 0; i < 1000; ++i) { + grnxx::RowID row_id = grnxx::MIN_ROW_ID + i; + if (!boolean_data[reference_data[i]]) { + assert(row_ids[count] == row_id); + assert(++count <= num_row_ids); + } + } + assert(count == num_row_ids); + } + + // Integer で絞り込む. + { + std::unique_ptr<grnxx::Calc> calc( + src_table->create_calc("Reference.Integer < 50")); + assert(calc); + std::vector<grnxx::RowID> row_ids(all_row_ids); + grnxx::Int64 num_row_ids = calc->filter(&*row_ids.begin(), row_ids.size()); + assert(num_row_ids != 0); + grnxx::Int64 count = 0; + for (grnxx::Int64 i = 0; i < 1000; ++i) { + grnxx::RowID row_id = grnxx::MIN_ROW_ID + i; + if (integer_data[reference_data[i]] < 50) { + assert(row_ids[count] == row_id); + assert(++count <= num_row_ids); + } + } + assert(count == num_row_ids); + } + + // Float で絞り込む. + { + std::unique_ptr<grnxx::Calc> calc( + src_table->create_calc("Reference.Float <= 0.5")); + assert(calc); + std::vector<grnxx::RowID> row_ids(all_row_ids); + grnxx::Int64 num_row_ids = calc->filter(&*row_ids.begin(), row_ids.size()); + assert(num_row_ids != 0); + grnxx::Int64 count = 0; + for (grnxx::Int64 i = 0; i < 1000; ++i) { + grnxx::RowID row_id = grnxx::MIN_ROW_ID + i; + if (float_data[reference_data[i]] <= 0.5) { + assert(row_ids[count] == row_id); + assert(++count <= num_row_ids); + } + } + assert(count == num_row_ids); + } + + // Boolean と Integer と Float の範囲で絞り込む. + { + std::unique_ptr<grnxx::Calc> calc(src_table->create_calc( + "Reference.Boolean && Reference.Integer < 50 && Reference.Float < 0.5")); + assert(calc); + std::vector<grnxx::RowID> row_ids(all_row_ids); + grnxx::Int64 num_row_ids = calc->filter(&*row_ids.begin(), row_ids.size()); + assert(num_row_ids != 0); + grnxx::Int64 count = 0; + for (grnxx::Int64 i = 0; i < 1000; ++i) { + grnxx::RowID row_id = grnxx::MIN_ROW_ID + i; + if (boolean_data[reference_data[i]] && + (integer_data[reference_data[i]] < 50) && + (float_data[reference_data[i]] < 0.5)) { + assert(row_ids[count] == row_id); + assert(++count <= num_row_ids); + } + } + assert(count == num_row_ids); + } +} + void test_sorter() { constexpr grnxx::Int64 DATA_SIZE = 1000; @@ -1208,6 +1354,7 @@ int main() { test_table(); test_column(); test_calc(); + test_reference(); test_sorter(); test_sorter_large(); test_index(); -------------- next part -------------- HTML����������������������������...Download