ClickHouse/src/AggregateFunctions/AggregateFunctionSumMap.h

570 lines
21 KiB
C++
Raw Normal View History

#pragma once
#include <IO/WriteHelpers.h>
#include <IO/ReadHelpers.h>
#include <DataTypes/DataTypeArray.h>
#include <DataTypes/DataTypeTuple.h>
2020-08-02 01:29:52 +00:00
#include <DataTypes/DataTypeNullable.h>
#include <Columns/ColumnArray.h>
#include <Columns/ColumnTuple.h>
#include <Columns/ColumnVector.h>
#include <Columns/ColumnDecimal.h>
#include <Columns/ColumnString.h>
2021-06-14 04:13:35 +00:00
#include <Common/FieldVisitorSum.h>
#include <Common/assert_cast.h>
#include <AggregateFunctions/IAggregateFunction.h>
2017-10-12 20:49:26 +00:00
#include <map>
namespace DB
{
struct Settings;
namespace ErrorCodes
{
2020-04-28 14:30:45 +00:00
extern const int BAD_ARGUMENTS;
2020-04-29 06:35:02 +00:00
extern const int ILLEGAL_TYPE_OF_ARGUMENT;
2020-06-16 10:44:23 +00:00
extern const int NUMBER_OF_ARGUMENTS_DOESNT_MATCH;
2020-08-02 14:22:53 +00:00
extern const int LOGICAL_ERROR;
}
template <typename T>
2020-06-15 18:53:54 +00:00
struct AggregateFunctionMapData
{
// Map needs to be ordered to maintain function properties
std::map<T, Array> merged_maps;
};
2017-10-12 20:49:59 +00:00
/** Aggregate function, that takes at least two arguments: keys and values, and as a result, builds a tuple of of at least 2 arrays -
2020-06-11 10:31:37 +00:00
* ordered keys and variable number of argument values aggregated by corresponding keys.
*
2020-06-11 10:31:37 +00:00
* sumMap function is the most useful when using SummingMergeTree to sum Nested columns, which name ends in "Map".
*
* Example: sumMap(k, v...) of:
* k v
* [1,2,3] [10,10,10]
* [3,4,5] [10,10,10]
* [4,5,6] [10,10,10]
* [6,7,8] [10,10,10]
* [7,5,3] [5,15,25]
* [8,9,10] [20,20,20]
* will return:
* ([1,2,3,4,5,6,7,8,9,10],[10,10,45,20,35,20,15,30,20,20])
2020-06-11 10:31:37 +00:00
*
* minMap and maxMap share the same idea, but calculate min and max correspondingly.
2021-01-04 01:54:00 +00:00
*
* NOTE: The implementation of these functions are "amateur grade" - not efficient and low quality.
*/
template <typename T, typename Derived, typename Visitor, bool overflow, bool tuple_argument, bool compact>
2020-06-15 18:53:54 +00:00
class AggregateFunctionMapBase : public IAggregateFunctionDataHelper<
AggregateFunctionMapData<NearestFieldType<T>>, Derived>
{
private:
DataTypePtr keys_type;
2021-03-13 18:05:18 +00:00
SerializationPtr keys_serialization;
2017-10-12 20:51:12 +00:00
DataTypes values_types;
2021-03-13 18:05:18 +00:00
Serializations values_serializations;
public:
2020-06-16 10:44:23 +00:00
using Base = IAggregateFunctionDataHelper<
AggregateFunctionMapData<NearestFieldType<T>>, Derived>;
2020-06-16 10:44:23 +00:00
AggregateFunctionMapBase(const DataTypePtr & keys_type_,
const DataTypes & values_types_, const DataTypes & argument_types_)
2021-03-13 18:05:18 +00:00
: Base(argument_types_, {} /* parameters */)
, keys_type(keys_type_)
, keys_serialization(keys_type->getDefaultSerialization())
, values_types(values_types_)
2020-12-26 17:01:36 +00:00
{
2021-03-13 18:05:18 +00:00
values_serializations.reserve(values_types.size());
for (const auto & type : values_types)
values_serializations.emplace_back(type->getDefaultSerialization());
2020-12-26 17:01:36 +00:00
}
DataTypePtr getReturnType() const override
{
DataTypes types;
types.emplace_back(std::make_shared<DataTypeArray>(keys_type));
for (const auto & value_type : values_types)
2020-04-28 14:30:45 +00:00
{
2020-12-25 19:22:42 +00:00
if constexpr (std::is_same_v<Visitor, FieldVisitorSum>)
{
if (!value_type->isSummable())
throw Exception{ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT,
"Values for {} cannot be summed, passed type {}",
getName(), value_type->getName()};
}
2020-04-28 14:30:45 +00:00
DataTypePtr result_type;
if constexpr (overflow)
{
2021-01-05 01:26:29 +00:00
if (value_type->onlyNull())
throw Exception{ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT,
"Cannot calculate {} of type {}",
getName(), value_type->getName()};
2020-04-28 14:30:45 +00:00
// Overflow, meaning that the returned type is the same as
2020-12-26 17:01:36 +00:00
// the input type. Nulls are skipped.
result_type = removeNullable(value_type);
2020-04-28 14:30:45 +00:00
}
else
2020-04-29 06:05:52 +00:00
{
2020-08-02 01:29:52 +00:00
auto value_type_without_nullable = removeNullable(value_type);
2020-04-28 14:30:45 +00:00
// No overflow, meaning we promote the types if necessary.
2020-08-02 01:29:52 +00:00
if (!value_type_without_nullable->canBePromoted())
throw Exception{ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT,
"Values for {} are expected to be Numeric, Float or Decimal, passed type {}",
getName(), value_type->getName()};
2020-04-28 14:30:45 +00:00
2021-02-19 16:25:50 +00:00
WhichDataType value_type_to_check(value_type);
2021-02-19 17:42:51 +00:00
2021-02-19 16:49:19 +00:00
/// Do not promote decimal because of implementation issues of this function design
/// Currently we cannot get result column type in case of decimal we cannot get decimal scale
/// in method void insertResultInto(AggregateDataPtr __restrict place, IColumn & to, Arena *) const override
2021-02-19 16:49:19 +00:00
/// If we decide to make this function more efficient we should promote decimal type during summ
2021-02-19 16:25:50 +00:00
if (value_type_to_check.isDecimal())
result_type = value_type_without_nullable;
else
result_type = value_type_without_nullable->promoteNumericType();
2020-04-28 14:30:45 +00:00
}
types.emplace_back(std::make_shared<DataTypeArray>(result_type));
}
return std::make_shared<DataTypeTuple>(types);
}
bool allocatesMemoryInArena() const override { return false; }
2020-04-29 06:05:52 +00:00
static const auto & getArgumentColumns(const IColumn**& columns)
{
if constexpr (tuple_argument)
{
2020-04-29 06:05:52 +00:00
return assert_cast<const ColumnTuple *>(columns[0])->getColumns();
}
else
{
2020-04-29 06:05:52 +00:00
return columns;
}
2020-04-29 06:05:52 +00:00
}
void add(AggregateDataPtr __restrict place, const IColumn ** columns_, const size_t row_num, Arena *) const override
2020-04-29 06:05:52 +00:00
{
2021-01-04 01:54:00 +00:00
const auto & columns = getArgumentColumns(columns_);
// Column 0 contains array of keys of known type
const ColumnArray & array_column0 = assert_cast<const ColumnArray &>(*columns[0]);
const IColumn::Offsets & offsets0 = array_column0.getOffsets();
const IColumn & key_column = array_column0.getData();
const size_t keys_vec_offset = offsets0[row_num - 1];
const size_t keys_vec_size = (offsets0[row_num] - keys_vec_offset);
// Columns 1..n contain arrays of numeric values to sum
auto & merged_maps = this->data(place).merged_maps;
2017-10-12 21:03:51 +00:00
for (size_t col = 0, size = values_types.size(); col < size; ++col)
{
2020-04-28 14:30:45 +00:00
const auto & array_column = assert_cast<const ColumnArray&>(*columns[col + 1]);
const IColumn & value_column = array_column.getData();
const IColumn::Offsets & offsets = array_column.getOffsets();
2018-12-24 14:26:38 +00:00
const size_t values_vec_offset = offsets[row_num - 1];
const size_t values_vec_size = (offsets[row_num] - values_vec_offset);
// Expect key and value arrays to be of same length
if (keys_vec_size != values_vec_size)
2020-04-28 14:30:45 +00:00
throw Exception("Sizes of keys and values arrays do not match", ErrorCodes::BAD_ARGUMENTS);
// Insert column values for all keys
for (size_t i = 0; i < keys_vec_size; ++i)
{
2021-01-04 01:54:00 +00:00
auto value = value_column[values_vec_offset + i];
auto key = key_column[keys_vec_offset + i].get<T>();
2019-01-22 16:47:43 +00:00
if (!keepKey(key))
continue;
2020-08-02 01:29:52 +00:00
2021-01-04 01:54:00 +00:00
decltype(merged_maps.begin()) it;
if constexpr (IsDecimalNumber<T>)
{
2020-04-28 14:30:45 +00:00
// FIXME why is storing NearestFieldType not enough, and we
// have to check for decimals again here?
UInt32 scale = static_cast<const ColumnDecimal<T> &>(key_column).getData().getScale();
it = merged_maps.find(DecimalField<T>(key, scale));
}
else
it = merged_maps.find(key);
2021-01-04 01:54:00 +00:00
if (it != merged_maps.end())
2020-04-28 14:30:45 +00:00
{
2021-01-04 01:54:00 +00:00
if (!value.isNull())
{
if (it->second[col].isNull())
it->second[col] = value;
else
applyVisitor(Visitor(value), it->second[col]);
}
2020-04-28 14:30:45 +00:00
}
else
{
// Create a value array for this key
Array new_values;
2021-01-05 01:26:29 +00:00
new_values.resize(size);
new_values[col] = value;
if constexpr (IsDecimalNumber<T>)
{
UInt32 scale = static_cast<const ColumnDecimal<T> &>(key_column).getData().getScale();
merged_maps.emplace(DecimalField<T>(key, scale), std::move(new_values));
}
else
2020-04-28 14:30:45 +00:00
{
merged_maps.emplace(key, std::move(new_values));
2020-04-28 14:30:45 +00:00
}
}
}
}
}
void merge(AggregateDataPtr __restrict place, ConstAggregateDataPtr rhs, Arena *) const override
{
auto & merged_maps = this->data(place).merged_maps;
const auto & rhs_maps = this->data(rhs).merged_maps;
for (const auto & elem : rhs_maps)
{
const auto & it = merged_maps.find(elem.first);
if (it != merged_maps.end())
{
for (size_t col = 0; col < values_types.size(); ++col)
2020-12-26 17:01:36 +00:00
if (!elem.second[col].isNull())
applyVisitor(Visitor(elem.second[col]), it->second[col]);
}
else
merged_maps[elem.first] = elem.second;
}
}
void serialize(ConstAggregateDataPtr __restrict place, WriteBuffer & buf) const override
{
const auto & merged_maps = this->data(place).merged_maps;
size_t size = merged_maps.size();
writeVarUInt(size, buf);
for (const auto & elem : merged_maps)
{
2021-03-13 18:05:18 +00:00
keys_serialization->serializeBinary(elem.first, buf);
for (size_t col = 0; col < values_types.size(); ++col)
2021-03-13 18:05:18 +00:00
values_serializations[col]->serializeBinary(elem.second[col], buf);
}
}
void deserialize(AggregateDataPtr __restrict place, ReadBuffer & buf, Arena *) const override
{
auto & merged_maps = this->data(place).merged_maps;
size_t size = 0;
readVarUInt(size, buf);
for (size_t i = 0; i < size; ++i)
{
Field key;
2021-03-13 18:05:18 +00:00
keys_serialization->deserializeBinary(key, buf);
Array values;
values.resize(values_types.size());
for (size_t col = 0; col < values_types.size(); ++col)
2021-03-13 18:05:18 +00:00
values_serializations[col]->deserializeBinary(values[col], buf);
if constexpr (IsDecimalNumber<T>)
merged_maps[key.get<DecimalField<T>>()] = values;
else
merged_maps[key.get<T>()] = values;
}
}
void insertResultInto(AggregateDataPtr __restrict place, IColumn & to, Arena *) const override
{
2021-01-04 01:54:00 +00:00
size_t num_columns = values_types.size();
// Final step does compaction of keys that have zero values, this mutates the state
auto & merged_maps = this->data(place).merged_maps;
2020-11-04 14:20:11 +00:00
// Remove keys which are zeros or empty. This should be enabled only for sumMap.
if constexpr (compact)
{
for (auto it = merged_maps.cbegin(); it != merged_maps.cend();)
{
// Key is not compacted if it has at least one non-zero value
bool erase = true;
2021-01-04 01:54:00 +00:00
for (size_t col = 0; col < num_columns; ++col)
{
2021-01-04 01:54:00 +00:00
if (!it->second[col].isNull() && it->second[col] != values_types[col]->getDefault())
{
erase = false;
break;
}
}
if (erase)
it = merged_maps.erase(it);
else
++it;
}
}
size_t size = merged_maps.size();
auto & to_tuple = assert_cast<ColumnTuple &>(to);
auto & to_keys_arr = assert_cast<ColumnArray &>(to_tuple.getColumn(0));
auto & to_keys_col = to_keys_arr.getData();
// Advance column offsets
auto & to_keys_offsets = to_keys_arr.getOffsets();
2019-01-08 10:07:33 +00:00
to_keys_offsets.push_back(to_keys_offsets.back() + size);
to_keys_col.reserve(size);
2021-01-04 01:54:00 +00:00
for (size_t col = 0; col < num_columns; ++col)
{
auto & to_values_arr = assert_cast<ColumnArray &>(to_tuple.getColumn(col + 1));
auto & to_values_offsets = to_values_arr.getOffsets();
2019-01-08 10:07:33 +00:00
to_values_offsets.push_back(to_values_offsets.back() + size);
to_values_arr.getData().reserve(size);
}
// Write arrays of keys and values
for (const auto & elem : merged_maps)
{
// Write array of keys into column
to_keys_col.insert(elem.first);
// Write 0..n arrays of values
2021-01-04 01:54:00 +00:00
for (size_t col = 0; col < num_columns; ++col)
{
auto & to_values_col = assert_cast<ColumnArray &>(to_tuple.getColumn(col + 1)).getData();
2021-01-04 01:54:00 +00:00
if (elem.second[col].isNull())
to_values_col.insertDefault();
else
to_values_col.insert(elem.second[col]);
}
}
}
bool keepKey(const T & key) const { return static_cast<const Derived &>(*this).keepKey(key); }
2020-06-11 10:31:37 +00:00
String getName() const override { return static_cast<const Derived &>(*this).getName(); }
2019-01-22 16:47:43 +00:00
};
2020-04-28 14:30:45 +00:00
template <typename T, bool overflow, bool tuple_argument>
2019-01-25 19:35:53 +00:00
class AggregateFunctionSumMap final :
public AggregateFunctionMapBase<T, AggregateFunctionSumMap<T, overflow, tuple_argument>, FieldVisitorSum, overflow, tuple_argument, true>
2019-01-22 16:47:43 +00:00
{
2019-01-25 19:35:53 +00:00
private:
2020-04-28 14:30:45 +00:00
using Self = AggregateFunctionSumMap<T, overflow, tuple_argument>;
using Base = AggregateFunctionMapBase<T, Self, FieldVisitorSum, overflow, tuple_argument, true>;
2019-01-25 19:35:53 +00:00
2019-01-22 16:47:43 +00:00
public:
2020-06-16 10:44:23 +00:00
AggregateFunctionSumMap(const DataTypePtr & keys_type_,
DataTypes & values_types_, const DataTypes & argument_types_,
const Array & params_)
: Base{keys_type_, values_types_, argument_types_}
{
// The constructor accepts parameters to have a uniform interface with
// sumMapFiltered, but this function doesn't have any parameters.
assertNoParameters(getName(), params_);
}
2019-01-22 16:47:43 +00:00
2021-05-05 15:11:56 +00:00
String getName() const override {
if constexpr (overflow)
{
return "sumMapWithOverflow";
}
else
{
return "sumMappedArrays";
}
}
2019-01-22 16:47:43 +00:00
bool keepKey(const T &) const { return true; }
2019-01-22 16:47:43 +00:00
};
2020-04-28 14:30:45 +00:00
template <typename T, bool overflow, bool tuple_argument>
2019-01-25 19:35:53 +00:00
class AggregateFunctionSumMapFiltered final :
2020-06-15 18:53:54 +00:00
public AggregateFunctionMapBase<T,
2020-04-28 14:30:45 +00:00
AggregateFunctionSumMapFiltered<T, overflow, tuple_argument>,
2020-06-11 10:31:37 +00:00
FieldVisitorSum,
2020-04-28 14:30:45 +00:00
overflow,
tuple_argument,
true>
2019-01-22 16:47:43 +00:00
{
private:
2020-04-28 14:30:45 +00:00
using Self = AggregateFunctionSumMapFiltered<T, overflow, tuple_argument>;
using Base = AggregateFunctionMapBase<T, Self, FieldVisitorSum, overflow, tuple_argument, true>;
2019-01-25 19:35:53 +00:00
/// ARCADIA_BUILD disallow unordered_set for big ints for some reason
static constexpr const bool allow_hash = !OverBigInt<T>;
using ContainerT = std::conditional_t<allow_hash, std::unordered_set<T>, std::set<T>>;
ContainerT keys_to_keep;
2019-01-22 16:47:43 +00:00
public:
2020-06-16 10:44:23 +00:00
AggregateFunctionSumMapFiltered(const DataTypePtr & keys_type_,
const DataTypes & values_types_, const DataTypes & argument_types_,
const Array & params_)
: Base{keys_type_, values_types_, argument_types_}
2019-01-22 16:47:43 +00:00
{
2020-06-16 10:44:23 +00:00
if (params_.size() != 1)
throw Exception(ErrorCodes::NUMBER_OF_ARGUMENTS_DOESNT_MATCH,
"Aggregate function '{}' requires exactly one parameter "
"of Array type", getName());
Array keys_to_keep_;
if (!params_.front().tryGet<Array>(keys_to_keep_))
throw Exception(ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT,
"Aggregate function {} requires an Array as a parameter",
getName());
if constexpr (allow_hash)
keys_to_keep.reserve(keys_to_keep_.size());
for (const Field & f : keys_to_keep_)
2019-01-22 16:47:43 +00:00
{
keys_to_keep.emplace(f.safeGet<T>());
2019-01-22 16:47:43 +00:00
}
}
2020-06-16 10:44:23 +00:00
String getName() const override
{ return overflow ? "sumMapFilteredWithOverflow" : "sumMapFiltered"; }
2019-01-22 16:47:43 +00:00
2019-01-24 12:31:33 +00:00
bool keepKey(const T & key) const { return keys_to_keep.count(key); }
};
2020-08-02 01:29:52 +00:00
/** Implements `Max` operation.
* Returns true if changed
*/
class FieldVisitorMax : public StaticVisitor<bool>
{
private:
const Field & rhs;
2021-06-15 16:24:18 +00:00
template <typename FieldType>
bool compareImpl(FieldType & x) const
2020-08-02 01:29:52 +00:00
{
2021-06-15 16:24:18 +00:00
auto val = get<FieldType>(rhs);
2020-08-02 01:29:52 +00:00
if (val > x)
{
x = val;
return true;
}
return false;
}
2021-06-15 16:24:18 +00:00
public:
explicit FieldVisitorMax(const Field & rhs_) : rhs(rhs_) {}
2020-08-02 01:29:52 +00:00
2021-06-15 16:24:18 +00:00
bool operator() (Null &) const { throw Exception("Cannot compare Nulls", ErrorCodes::LOGICAL_ERROR); }
bool operator() (NegativeInfinity &) const { throw Exception("Cannot compare -Inf", ErrorCodes::LOGICAL_ERROR); }
bool operator() (PositiveInfinity &) const { throw Exception("Cannot compare +Inf", ErrorCodes::LOGICAL_ERROR); }
2021-06-15 16:24:18 +00:00
bool operator() (AggregateFunctionStateData &) const { throw Exception("Cannot compare AggregateFunctionStates", ErrorCodes::LOGICAL_ERROR); }
bool operator() (Array & x) const { return compareImpl<Array>(x); }
bool operator() (Tuple & x) const { return compareImpl<Tuple>(x); }
template <typename T>
bool operator() (DecimalField<T> & x) const { return compareImpl<DecimalField<T>>(x); }
template <typename T>
bool operator() (T & x) const { return compareImpl<T>(x); }
2020-08-02 01:29:52 +00:00
};
/** Implements `Min` operation.
* Returns true if changed
*/
class FieldVisitorMin : public StaticVisitor<bool>
{
private:
const Field & rhs;
2021-06-15 16:24:18 +00:00
template <typename FieldType>
bool compareImpl(FieldType & x) const
2020-08-02 01:29:52 +00:00
{
2021-06-15 16:24:18 +00:00
auto val = get<FieldType>(rhs);
2020-08-02 01:29:52 +00:00
if (val < x)
{
x = val;
return true;
}
return false;
}
2021-06-15 16:24:18 +00:00
public:
explicit FieldVisitorMin(const Field & rhs_) : rhs(rhs_) {}
2020-08-02 01:29:52 +00:00
2021-06-15 16:24:18 +00:00
bool operator() (Null &) const { throw Exception("Cannot compare Nulls", ErrorCodes::LOGICAL_ERROR); }
bool operator() (NegativeInfinity &) const { throw Exception("Cannot compare -Inf", ErrorCodes::LOGICAL_ERROR); }
bool operator() (PositiveInfinity &) const { throw Exception("Cannot compare +Inf", ErrorCodes::LOGICAL_ERROR); }
2021-06-15 16:24:18 +00:00
bool operator() (AggregateFunctionStateData &) const { throw Exception("Cannot sum AggregateFunctionStates", ErrorCodes::LOGICAL_ERROR); }
bool operator() (Array & x) const { return compareImpl<Array>(x); }
bool operator() (Tuple & x) const { return compareImpl<Tuple>(x); }
template <typename T>
bool operator() (DecimalField<T> & x) const { return compareImpl<DecimalField<T>>(x); }
template <typename T>
bool operator() (T & x) const { return compareImpl<T>(x); }
2020-08-02 01:29:52 +00:00
};
2020-06-11 10:31:37 +00:00
template <typename T, bool tuple_argument>
class AggregateFunctionMinMap final :
public AggregateFunctionMapBase<T, AggregateFunctionMinMap<T, tuple_argument>, FieldVisitorMin, true, tuple_argument, false>
2020-06-11 10:31:37 +00:00
{
private:
using Self = AggregateFunctionMinMap<T, tuple_argument>;
using Base = AggregateFunctionMapBase<T, Self, FieldVisitorMin, true, tuple_argument, false>;
2020-06-11 10:31:37 +00:00
public:
2020-06-16 10:44:23 +00:00
AggregateFunctionMinMap(const DataTypePtr & keys_type_,
DataTypes & values_types_, const DataTypes & argument_types_,
const Array & params_)
: Base{keys_type_, values_types_, argument_types_}
{
// The constructor accepts parameters to have a uniform interface with
// sumMapFiltered, but this function doesn't have any parameters.
assertNoParameters(getName(), params_);
}
2020-06-11 10:31:37 +00:00
2021-05-05 15:11:56 +00:00
String getName() const override { return "minMappedArrays"; }
2020-06-11 10:31:37 +00:00
bool keepKey(const T &) const { return true; }
};
template <typename T, bool tuple_argument>
class AggregateFunctionMaxMap final :
public AggregateFunctionMapBase<T, AggregateFunctionMaxMap<T, tuple_argument>, FieldVisitorMax, true, tuple_argument, false>
2020-06-11 10:31:37 +00:00
{
private:
using Self = AggregateFunctionMaxMap<T, tuple_argument>;
using Base = AggregateFunctionMapBase<T, Self, FieldVisitorMax, true, tuple_argument, false>;
2020-06-11 10:31:37 +00:00
public:
2020-06-16 10:44:23 +00:00
AggregateFunctionMaxMap(const DataTypePtr & keys_type_,
DataTypes & values_types_, const DataTypes & argument_types_,
const Array & params_)
: Base{keys_type_, values_types_, argument_types_}
{
// The constructor accepts parameters to have a uniform interface with
// sumMapFiltered, but this function doesn't have any parameters.
assertNoParameters(getName(), params_);
}
2020-06-11 10:31:37 +00:00
2021-05-05 15:11:56 +00:00
String getName() const override { return "maxMappedArrays"; }
2020-06-11 10:31:37 +00:00
bool keepKey(const T &) const { return true; }
};
}