585 lines
23 KiB
C++
585 lines
23 KiB
C++
/*
|
|
Copyright 2013--2018 James E. McClure, Virginia Polytechnic & State University
|
|
Copyright Equnior ASA
|
|
|
|
This file is part of the Open Porous Media project (OPM).
|
|
OPM is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
OPM is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
You should have received a copy of the GNU General Public License
|
|
along with OPM. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "common/Database.h"
|
|
#include "common/Utilities.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <tuple>
|
|
|
|
/********************************************************************
|
|
* Constructors/destructor *
|
|
********************************************************************/
|
|
Database::Database() = default;
|
|
Database::~Database() = default;
|
|
Database::Database(const Database &rhs) : KeyData(rhs) {
|
|
d_data.clear();
|
|
for (const auto &tmp : rhs.d_data)
|
|
putData(tmp.first, tmp.second->clone());
|
|
}
|
|
Database &Database::operator=(const Database &rhs) {
|
|
if (this == &rhs)
|
|
return *this;
|
|
d_data.clear();
|
|
for (const auto &tmp : rhs.d_data)
|
|
putData(tmp.first, tmp.second->clone());
|
|
return *this;
|
|
}
|
|
Database::Database(Database &&rhs) { std::swap(d_data, rhs.d_data); }
|
|
Database &Database::operator=(Database &&rhs) {
|
|
if (this != &rhs)
|
|
std::swap(d_data, rhs.d_data);
|
|
return *this;
|
|
}
|
|
|
|
/********************************************************************
|
|
* Clone the database *
|
|
********************************************************************/
|
|
std::shared_ptr<KeyData> Database::clone() const { return cloneDatabase(); }
|
|
std::shared_ptr<Database> Database::cloneDatabase() const {
|
|
auto db = std::make_shared<Database>();
|
|
for (const auto &tmp : d_data)
|
|
db->putData(tmp.first, tmp.second->clone());
|
|
return db;
|
|
}
|
|
|
|
/********************************************************************
|
|
* Get the data object *
|
|
********************************************************************/
|
|
bool Database::keyExists(const std::string &key) const {
|
|
return d_data.find(key) != d_data.end();
|
|
}
|
|
std::shared_ptr<KeyData> Database::getData(const std::string &key) {
|
|
auto it = d_data.find(key);
|
|
if (it == d_data.end()) {
|
|
char msg[1000];
|
|
sprintf(msg, "Variable %s was not found in database", key.c_str());
|
|
ERROR(msg);
|
|
}
|
|
return it->second;
|
|
}
|
|
std::shared_ptr<const KeyData> Database::getData(const std::string &key) const {
|
|
return const_cast<Database *>(this)->getData(key);
|
|
}
|
|
bool Database::isDatabase(const std::string &key) const {
|
|
auto ptr = getData(key);
|
|
auto ptr2 = std::dynamic_pointer_cast<const Database>(ptr);
|
|
return ptr2 != nullptr;
|
|
}
|
|
std::shared_ptr<Database> Database::getDatabase(const std::string &key) {
|
|
std::shared_ptr<KeyData> ptr = getData(key);
|
|
std::shared_ptr<Database> ptr2 = std::dynamic_pointer_cast<Database>(ptr);
|
|
if (ptr2 == nullptr) {
|
|
char msg[1000];
|
|
sprintf(msg, "Variable %s is not a database", key.c_str());
|
|
ERROR(msg);
|
|
}
|
|
return ptr2;
|
|
}
|
|
std::shared_ptr<const Database>
|
|
Database::getDatabase(const std::string &key) const {
|
|
return const_cast<Database *>(this)->getDatabase(key);
|
|
}
|
|
std::vector<std::string> Database::getAllKeys() const {
|
|
std::vector<std::string> keys;
|
|
keys.reserve(d_data.size());
|
|
for (const auto &it : d_data)
|
|
keys.push_back(it.first);
|
|
return keys;
|
|
}
|
|
void Database::putDatabase(const std::string &key,
|
|
std::shared_ptr<Database> db) {
|
|
d_data[key] = std::move(db);
|
|
}
|
|
void Database::putData(const std::string &key, std::shared_ptr<KeyData> data) {
|
|
d_data[key] = std::move(data);
|
|
}
|
|
|
|
/********************************************************************
|
|
* Is the data of the given type *
|
|
********************************************************************/
|
|
template <> bool Database::isType<double>(const std::string &key) const {
|
|
auto type = getData(key)->type();
|
|
return type == "double";
|
|
}
|
|
template <> bool Database::isType<float>(const std::string &key) const {
|
|
auto type = getData(key)->type();
|
|
return type == "double";
|
|
}
|
|
template <> bool Database::isType<int>(const std::string &key) const {
|
|
bool pass = true;
|
|
auto type = getData(key)->type();
|
|
if (type == "double") {
|
|
auto data = getVector<double>(key);
|
|
for (auto tmp : data)
|
|
pass = pass && static_cast<double>(static_cast<int>(tmp)) == tmp;
|
|
} else {
|
|
pass = false;
|
|
}
|
|
return pass;
|
|
}
|
|
template <> bool Database::isType<std::string>(const std::string &key) const {
|
|
auto type = getData(key)->type();
|
|
return type == "string";
|
|
}
|
|
template <> bool Database::isType<bool>(const std::string &key) const {
|
|
auto type = getData(key)->type();
|
|
return type == "bool";
|
|
}
|
|
|
|
/********************************************************************
|
|
* Get a vector *
|
|
********************************************************************/
|
|
template <>
|
|
std::vector<std::string>
|
|
Database::getVector<std::string>(const std::string &key, const Units &) const {
|
|
std::shared_ptr<const KeyData> ptr = getData(key);
|
|
if (std::dynamic_pointer_cast<const EmptyKeyData>(ptr))
|
|
return std::vector<std::string>();
|
|
const auto *ptr2 = dynamic_cast<const KeyDataString *>(ptr.get());
|
|
if (ptr2 == nullptr) {
|
|
ERROR("Key '" + key + "' is not a string");
|
|
}
|
|
return ptr2->d_data;
|
|
}
|
|
template <>
|
|
std::vector<bool> Database::getVector<bool>(const std::string &key,
|
|
const Units &) const {
|
|
std::shared_ptr<const KeyData> ptr = getData(key);
|
|
if (std::dynamic_pointer_cast<const EmptyKeyData>(ptr))
|
|
return std::vector<bool>();
|
|
const auto *ptr2 = dynamic_cast<const KeyDataBool *>(ptr.get());
|
|
if (ptr2 == nullptr) {
|
|
ERROR("Key '" + key + "' is not a bool");
|
|
}
|
|
return ptr2->d_data;
|
|
}
|
|
template <class TYPE>
|
|
std::vector<TYPE> Database::getVector(const std::string &key,
|
|
const Units &unit) const {
|
|
std::shared_ptr<const KeyData> ptr = getData(key);
|
|
if (std::dynamic_pointer_cast<const EmptyKeyData>(ptr))
|
|
return std::vector<TYPE>();
|
|
std::vector<TYPE> data;
|
|
if (std::dynamic_pointer_cast<const KeyDataDouble>(ptr)) {
|
|
const auto *ptr2 = dynamic_cast<const KeyDataDouble *>(ptr.get());
|
|
const std::vector<double> &data2 = ptr2->d_data;
|
|
double factor = 1;
|
|
if (!unit.isNull()) {
|
|
INSIST(!ptr2->d_unit.isNull(), "Field " + key + " must have units");
|
|
factor = ptr2->d_unit.convert(unit);
|
|
INSIST(factor != 0, "Unit conversion failed");
|
|
}
|
|
data.resize(data2.size());
|
|
for (size_t i = 0; i < data2.size(); i++)
|
|
data[i] = static_cast<TYPE>(factor * data2[i]);
|
|
} else if (std::dynamic_pointer_cast<const KeyDataString>(ptr)) {
|
|
ERROR("Converting std::string to another type");
|
|
} else if (std::dynamic_pointer_cast<const KeyDataBool>(ptr)) {
|
|
ERROR("Converting std::bool to another type");
|
|
} else {
|
|
ERROR("Unable to convert data format");
|
|
}
|
|
return data;
|
|
}
|
|
|
|
/********************************************************************
|
|
* Put a vector *
|
|
********************************************************************/
|
|
template <>
|
|
void Database::putVector<std::string>(const std::string &key,
|
|
const std::vector<std::string> &data,
|
|
const Units &) {
|
|
std::shared_ptr<KeyDataString> ptr(new KeyDataString());
|
|
ptr->d_data = data;
|
|
d_data[key] = ptr;
|
|
}
|
|
template <>
|
|
void Database::putVector<bool>(const std::string &key,
|
|
const std::vector<bool> &data, const Units &) {
|
|
std::shared_ptr<KeyDataBool> ptr(new KeyDataBool());
|
|
ptr->d_data = data;
|
|
d_data[key] = ptr;
|
|
}
|
|
template <class TYPE>
|
|
void Database::putVector(const std::string &key, const std::vector<TYPE> &data,
|
|
const Units &unit) {
|
|
std::shared_ptr<KeyDataDouble> ptr(new KeyDataDouble());
|
|
ptr->d_unit = unit;
|
|
ptr->d_data.resize(data.size());
|
|
for (size_t i = 0; i < data.size(); i++)
|
|
ptr->d_data[i] = static_cast<double>(data[i]);
|
|
d_data[key] = ptr;
|
|
}
|
|
|
|
/********************************************************************
|
|
* Print the database *
|
|
********************************************************************/
|
|
void Database::print(std::ostream &os, const std::string &indent) const {
|
|
for (const auto &it : d_data) {
|
|
os << indent << it.first;
|
|
if (dynamic_cast<const Database *>(it.second.get())) {
|
|
const auto *db = dynamic_cast<const Database *>(it.second.get());
|
|
os << " {\n";
|
|
db->print(os, indent + " ");
|
|
os << indent << "}\n";
|
|
} else {
|
|
os << " = ";
|
|
it.second->print(os, "");
|
|
}
|
|
}
|
|
}
|
|
std::string Database::print(const std::string &indent) const {
|
|
std::stringstream ss;
|
|
print(ss, indent);
|
|
return ss.str();
|
|
}
|
|
|
|
/********************************************************************
|
|
* Read input database file *
|
|
********************************************************************/
|
|
Database::Database(const std::string &filename) {
|
|
// Read the input file into memory
|
|
FILE *fid = fopen(filename.c_str(), "rb");
|
|
if (fid == nullptr)
|
|
ERROR("Error opening file " + filename);
|
|
fseek(fid, 0, SEEK_END);
|
|
size_t bytes = ftell(fid);
|
|
rewind(fid);
|
|
auto *buffer = new char[bytes + 4];
|
|
size_t result = fread(buffer, 1, bytes, fid);
|
|
fclose(fid);
|
|
if (result != bytes)
|
|
ERROR("Error reading file " + filename);
|
|
buffer[bytes + 0] = '\n';
|
|
buffer[bytes + 1] = '}';
|
|
buffer[bytes + 2] = '\n';
|
|
buffer[bytes + 3] = 0;
|
|
// Create the database entries
|
|
loadDatabase(buffer, *this);
|
|
// Free temporary memory
|
|
delete[] buffer;
|
|
}
|
|
std::shared_ptr<Database> Database::createFromString(const std::string &data) {
|
|
std::shared_ptr<Database> db(new Database());
|
|
auto *buffer = new char[data.size() + 4];
|
|
memcpy(buffer, data.data(), data.size());
|
|
buffer[data.size() + 0] = '\n';
|
|
buffer[data.size() + 1] = '}';
|
|
buffer[data.size() + 2] = '\n';
|
|
buffer[data.size() + 3] = 0;
|
|
loadDatabase(buffer, *db);
|
|
delete[] buffer;
|
|
return db;
|
|
}
|
|
enum class token_type {
|
|
newline,
|
|
line_comment,
|
|
block_start,
|
|
block_stop,
|
|
quote,
|
|
equal,
|
|
bracket,
|
|
end_bracket,
|
|
end
|
|
};
|
|
inline size_t length(token_type type) {
|
|
size_t len = 0;
|
|
if (type == token_type::newline || type == token_type::quote ||
|
|
type == token_type::equal || type == token_type::bracket ||
|
|
type == token_type::end_bracket || type == token_type::end) {
|
|
len = 1;
|
|
} else if (type == token_type::line_comment ||
|
|
type == token_type::block_start ||
|
|
type == token_type::block_stop) {
|
|
len = 2;
|
|
}
|
|
return len;
|
|
}
|
|
inline std::tuple<size_t, token_type> find_next_token(const char *buffer) {
|
|
size_t i = 0;
|
|
while (true) {
|
|
if (buffer[i] == '\n' || buffer[i] == '\r') {
|
|
return std::pair<size_t, token_type>(i + 1, token_type::newline);
|
|
} else if (buffer[i] == 0) {
|
|
return std::pair<size_t, token_type>(i + 1, token_type::end);
|
|
} else if (buffer[i] == '"') {
|
|
return std::pair<size_t, token_type>(i + 1, token_type::quote);
|
|
} else if (buffer[i] == '=') {
|
|
return std::pair<size_t, token_type>(i + 1, token_type::equal);
|
|
} else if (buffer[i] == '{') {
|
|
return std::pair<size_t, token_type>(i + 1, token_type::bracket);
|
|
} else if (buffer[i] == '}') {
|
|
return std::pair<size_t, token_type>(i + 1,
|
|
token_type::end_bracket);
|
|
} else if (buffer[i] == '/') {
|
|
if (buffer[i + 1] == '/') {
|
|
return std::pair<size_t, token_type>(i + 2,
|
|
token_type::line_comment);
|
|
} else if (buffer[i + 1] == '*') {
|
|
return std::pair<size_t, token_type>(i + 2,
|
|
token_type::block_start);
|
|
}
|
|
} else if (buffer[i] == '*') {
|
|
if (buffer[i + 1] == '/')
|
|
return std::pair<size_t, token_type>(i + 2,
|
|
token_type::block_stop);
|
|
}
|
|
i++;
|
|
}
|
|
return std::pair<size_t, token_type>(0, token_type::end);
|
|
}
|
|
inline std::string deblank(const std::string &str) {
|
|
size_t i1 = 0xFFFFFFF, i2 = 0;
|
|
for (size_t i = 0; i < str.size(); i++) {
|
|
if (str[i] != ' ') {
|
|
i1 = std::min(i1, i);
|
|
i2 = std::max(i2, i);
|
|
}
|
|
}
|
|
return i1 <= i2 ? str.substr(i1, i2 - i1 + 1) : std::string();
|
|
}
|
|
size_t skip_comment(const char *buffer) {
|
|
auto tmp = find_next_token(buffer);
|
|
const token_type end_comment =
|
|
(std::get<1>(tmp) == token_type::line_comment) ? token_type::newline
|
|
: token_type::block_stop;
|
|
size_t pos = 0;
|
|
while (std::get<1>(tmp) != end_comment) {
|
|
if (std::get<1>(tmp) == token_type::end)
|
|
ERROR("Encountered end of file before block comment end");
|
|
pos += std::get<0>(tmp);
|
|
tmp = find_next_token(&buffer[pos]);
|
|
}
|
|
pos += std::get<0>(tmp);
|
|
return pos;
|
|
}
|
|
inline std::string lower(const std::string &str) {
|
|
std::string tmp(str);
|
|
std::transform(tmp.begin(), tmp.end(), tmp.begin(), ::tolower);
|
|
return tmp;
|
|
}
|
|
static std::tuple<size_t, std::shared_ptr<KeyData>>
|
|
read_value(const char *buffer, const std::string &key) {
|
|
// Get the value as a std::string
|
|
size_t pos = 0;
|
|
token_type type = token_type::end;
|
|
std::tie(pos, type) = find_next_token(&buffer[pos]);
|
|
size_t len = pos - length(type);
|
|
while (type != token_type::newline) {
|
|
if (type == token_type::quote) {
|
|
size_t i = 0;
|
|
std::tie(i, type) = find_next_token(&buffer[pos]);
|
|
pos += i;
|
|
while (type != token_type::quote) {
|
|
ASSERT(type != token_type::end);
|
|
std::tie(i, type) = find_next_token(&buffer[pos]);
|
|
pos += i;
|
|
}
|
|
} else if (type == token_type::line_comment ||
|
|
type == token_type::block_start) {
|
|
len = pos - length(type);
|
|
pos += skip_comment(&buffer[pos - length(type)]) - length(type);
|
|
break;
|
|
}
|
|
size_t i = 0;
|
|
std::tie(i, type) = find_next_token(&buffer[pos]);
|
|
pos += i;
|
|
len = pos - length(type);
|
|
}
|
|
const std::string value = deblank(std::string(buffer, len));
|
|
// Split the value to an array of values
|
|
std::vector<std::string> values;
|
|
size_t i0 = 0, i = 0, count = 0;
|
|
for (; i < value.size(); i++) {
|
|
if (value[i] == '"') {
|
|
count++;
|
|
} else if (value[i] == ',' && count % 2 == 0) {
|
|
values.push_back(deblank(value.substr(i0, i - i0)));
|
|
i0 = i + 1;
|
|
}
|
|
}
|
|
values.push_back(deblank(value.substr(i0)));
|
|
// Convert the string value to the database value
|
|
std::shared_ptr<KeyData> data;
|
|
if (value.empty()) {
|
|
data.reset(new EmptyKeyData());
|
|
} else if (value.find('"') != std::string::npos) {
|
|
auto *data2 = new KeyDataString();
|
|
data.reset(data2);
|
|
data2->d_data.resize(values.size());
|
|
for (size_t i = 0; i < values.size(); i++) {
|
|
ASSERT(values[i].size() >= 2);
|
|
ASSERT(values[i][0] == '"' &&
|
|
values[i][values[i].size() - 1] == '"');
|
|
data2->d_data[i] = values[i].substr(1, values[i].size() - 2);
|
|
}
|
|
} else if (lower(value) == "true" || lower(value) == "false") {
|
|
auto *data2 = new KeyDataBool();
|
|
data.reset(data2);
|
|
data2->d_data.resize(values.size());
|
|
for (size_t i = 0; i < values.size(); i++) {
|
|
ASSERT(values[i].size() >= 2);
|
|
if (lower(values[i]) != "true" && lower(values[i]) != "false")
|
|
ERROR("Error converting " + key + " to logical array");
|
|
data2->d_data[i] = lower(values[i]) == "true";
|
|
}
|
|
} else { // if ( value.find('.')!=std::string::npos || value.find('e')!=std::string::npos ) {
|
|
auto *data2 = new KeyDataDouble();
|
|
data.reset(data2);
|
|
data2->d_data.resize(values.size(), 0);
|
|
for (size_t i = 0; i < values.size(); i++) {
|
|
Units unit;
|
|
std::tie(data2->d_data[i], unit) = KeyDataDouble::read(values[i]);
|
|
if (!unit.isNull())
|
|
data2->d_unit = unit;
|
|
}
|
|
//} else {
|
|
// ERROR("Unable to determine data type: "+value);
|
|
}
|
|
return std::tuple<size_t, std::shared_ptr<KeyData>>(pos, data);
|
|
}
|
|
size_t Database::loadDatabase(const char *buffer, Database &db) {
|
|
size_t pos = 0;
|
|
while (true) {
|
|
size_t i;
|
|
token_type type;
|
|
std::tie(i, type) = find_next_token(&buffer[pos]);
|
|
const std::string key = deblank(
|
|
std::string(&buffer[pos], std::max<int>(i - length(type), 1) - 1));
|
|
if (type == token_type::line_comment ||
|
|
type == token_type::block_start) {
|
|
// Comment
|
|
INSIST(key.empty(), "Key should be empty: " + key);
|
|
pos += skip_comment(&buffer[pos]);
|
|
} else if (type == token_type::newline) {
|
|
INSIST(key.empty(), "Key should be empty: " + key);
|
|
pos += i;
|
|
} else if (type == token_type::equal) {
|
|
// Reading key/value pair
|
|
ASSERT(!key.empty());
|
|
pos += i;
|
|
std::shared_ptr<KeyData> data;
|
|
std::tie(i, data) = read_value(&buffer[pos], key);
|
|
ASSERT(data.get() != nullptr);
|
|
db.d_data[key] = data;
|
|
pos += i;
|
|
} else if (type == token_type::bracket) {
|
|
// Read database
|
|
ASSERT(!key.empty());
|
|
pos += i;
|
|
std::shared_ptr<Database> database(new Database());
|
|
pos += loadDatabase(&buffer[pos], *database);
|
|
db.d_data[key] = database;
|
|
} else if (type == token_type::end_bracket) {
|
|
// Finished with the database
|
|
pos += i;
|
|
break;
|
|
} else {
|
|
ERROR("Error loading data");
|
|
}
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
/********************************************************************
|
|
* Data type helper functions *
|
|
********************************************************************/
|
|
void KeyDataDouble::print(std::ostream &os, const std::string &indent) const {
|
|
os << indent;
|
|
for (size_t i = 0; i < d_data.size(); i++) {
|
|
if (i > 0)
|
|
os << ", ";
|
|
if (d_data[i] != d_data[i]) {
|
|
os << "nan";
|
|
} else if (d_data[i] == std::numeric_limits<double>::infinity()) {
|
|
os << "inf";
|
|
} else if (d_data[i] == -std::numeric_limits<double>::infinity()) {
|
|
os << "-inf";
|
|
} else {
|
|
os << std::setprecision(12) << d_data[i];
|
|
}
|
|
}
|
|
if (!d_unit.isNull())
|
|
os << " " << d_unit.str();
|
|
os << std::endl;
|
|
}
|
|
std::tuple<double, Units> KeyDataDouble::read(const std::string &str) {
|
|
std::string tmp = deblank(str);
|
|
size_t index = tmp.find(" ");
|
|
if (index != std::string::npos) {
|
|
return std::make_tuple(readValue(tmp.substr(0, index)),
|
|
Units(tmp.substr(index + 1)));
|
|
} else {
|
|
return std::make_tuple(readValue(tmp), Units());
|
|
}
|
|
}
|
|
double KeyDataDouble::readValue(const std::string &str) {
|
|
const std::string tmp = lower(str);
|
|
double data = 0;
|
|
if (tmp == "inf" || tmp == "infinity") {
|
|
data = std::numeric_limits<double>::infinity();
|
|
} else if (tmp == "-inf" || tmp == "-infinity") {
|
|
data = -std::numeric_limits<double>::infinity();
|
|
} else if (tmp == "nan") {
|
|
data = std::numeric_limits<double>::quiet_NaN();
|
|
} else if (tmp.find('/') != std::string::npos) {
|
|
ERROR("Error reading value");
|
|
} else {
|
|
char *pos = nullptr;
|
|
data = strtod(tmp.c_str(), &pos);
|
|
if (static_cast<size_t>(pos - tmp.c_str()) == tmp.size() + 1)
|
|
ERROR("Error reading value");
|
|
}
|
|
return data;
|
|
}
|
|
|
|
/********************************************************************
|
|
* Instantiations *
|
|
********************************************************************/
|
|
template std::vector<char> Database::getVector<char>(const std::string &,
|
|
const Units &) const;
|
|
template std::vector<int> Database::getVector<int>(const std::string &,
|
|
const Units &) const;
|
|
template std::vector<size_t> Database::getVector<size_t>(const std::string &,
|
|
const Units &) const;
|
|
template std::vector<float> Database::getVector<float>(const std::string &,
|
|
const Units &) const;
|
|
template std::vector<double> Database::getVector<double>(const std::string &,
|
|
const Units &) const;
|
|
template void Database::putVector<char>(const std::string &,
|
|
const std::vector<char> &,
|
|
const Units &);
|
|
template void Database::putVector<int>(const std::string &,
|
|
const std::vector<int> &, const Units &);
|
|
template void Database::putVector<size_t>(const std::string &,
|
|
const std::vector<size_t> &,
|
|
const Units &);
|
|
template void Database::putVector<float>(const std::string &,
|
|
const std::vector<float> &,
|
|
const Units &);
|
|
template void Database::putVector<double>(const std::string &,
|
|
const std::vector<double> &,
|
|
const Units &);
|
|
template bool Database::isType<int>(const std::string &) const;
|
|
template bool Database::isType<float>(const std::string &) const;
|
|
template bool Database::isType<double>(const std::string &) const;
|
|
template bool Database::isType<std::string>(const std::string &) const;
|