15 #include <fmt/format.h>
19 inline constexpr
bool always_false_v =
false;
21 char const* TypeStr() noexcept;
23 char const* TypeStr<uint64_t>() noexcept {
27 char const* TypeStr<int64_t>() noexcept {
31 char const* TypeStr<double>() noexcept {
37 using namespace std::string_view_literals;
39 [&](
auto& value) -> std::string {
40 using T = std::decay_t<decltype(value)>;
41 if constexpr (std::is_same_v<T, std::string>) {
44 return fmt::format(
"'{: <8s}'", value);
46 return fmt::format(
"'{}'", value);
48 }
else if constexpr (std::is_same_v<T, bool>) {
49 return value ?
"T" :
"F";
50 }
else if constexpr (std::is_same_v<T, double>) {
52 return fmt::format(
"{:g}", value);
54 return fmt::format(
"{:.1f}", value);
56 }
else if constexpr (std::is_same_v<T, int64_t>) {
57 return fmt::format(
"{:d}", value);
58 }
else if constexpr (std::is_same_v<T, uint64_t>) {
59 return fmt::format(
"{:d}", value);
61 static_assert(always_false_v<T>,
"non-exhaustive visitor!");
67 constexpr
char const* FMT_VALUEKW_SCALAR =
"{:<8}= {:>20}";
68 constexpr
char const* FMT_VALUEKW_STR =
"{:<8}= {:<20}";
69 constexpr
char const* FMT_ESOKW =
"HIERARCH ESO {} = {}";
70 constexpr
char const* FMT_ALL_COMMENT =
"{} / {}";
71 constexpr
char const* FMT_COMMENTARYKW =
"{:<8} {}";
77 LiteralKeyword InternalValueKeywordFormat(std::string_view name,
78 std::string_view value,
79 std::string_view comment,
82 auto formatted_name_value =
83 fmt::format(is_string ? FMT_VALUEKW_STR : FMT_VALUEKW_SCALAR, name, value);
84 if (formatted_name_value.size() > constants::RECORD_LENGTH) {
85 throw std::invalid_argument(fmt::format(
86 "Formatted name and value does not fit in keyword record. Length: {}, keyword: '{}'",
87 formatted_name_value.size(),
88 formatted_name_value));
90 auto formatted = fmt::format(FMT_ALL_COMMENT, formatted_name_value, comment);
91 auto view = std::string_view(formatted).substr(0u, fits::constants::RECORD_LENGTH);
92 return LiteralKeyword(view);
96 InternalEsoKeywordFormat(std::string_view name, std::string_view value, std::string_view comment) {
97 auto formatted_name_value = fmt::format(FMT_ESOKW, name, value);
98 if (formatted_name_value.size() > constants::RECORD_LENGTH) {
99 throw std::invalid_argument(fmt::format(
100 "Formatted name and value does not fit in keyword record. Length: {}, keyword: '{}'",
101 formatted_name_value.size(),
102 formatted_name_value));
104 auto formatted = fmt::format(FMT_ALL_COMMENT, formatted_name_value, comment);
105 auto view = std::string_view(formatted).substr(0u, fits::constants::RECORD_LENGTH);
106 return LiteralKeyword(view);
109 LiteralKeyword InternalCommentaryKeywordFormat(std::string_view name, std::string_view value) {
110 auto formatted_name_value = fmt::format(FMT_COMMENTARYKW, name, value);
111 if (formatted_name_value.size() > constants::RECORD_LENGTH) {
112 throw std::invalid_argument(fmt::format(
113 "Formatted name and value does not fit in keyword record. Length: {}, keyword: '{}'",
114 formatted_name_value.size(),
115 formatted_name_value));
117 return LiteralKeyword(formatted_name_value);
138 void ValidateKeywordName(KeywordNameView keyword) {
139 if (keyword.name.empty()) {
140 throw std::invalid_argument(
"Keyword name cannot be empty");
142 auto max_length = keyword.type ==
KeywordType::Eso ? constants::ESO_HIERARCH_MAX_NAME_LENGTH
143 : constants::KEYWORD_NAME_LENGTH;
144 if (keyword.name.size() > max_length) {
145 auto msg = fmt::format(
"Keyword name too long. Maximum is {} and provided name '{}' is {}",
148 keyword.name.size());
149 throw std::invalid_argument(msg);
151 auto is_valid_char = [](
char c) ->
bool {
152 return (c >=
'0' && c <=
'9') || (c >=
'A' && c <=
'Z') || c ==
' ' || c ==
'_' || c ==
'-';
155 if (!std::all_of(std::begin(keyword.name), std::end(keyword.name), is_valid_char)) {
156 throw std::invalid_argument(fmt::format(
157 "Keyword name '{}' contains illegal character. Valid charset is [A-Z0-9_-].",
162 constexpr
KeywordType ParseKeywordType(std::string_view record) noexcept {
163 if (record.size() >= constants::KEYWORD_NAME_LENGTH + constants::VALUE_INDICATOR.size()) {
165 if (record.substr(constants::KEYWORD_NAME_LENGTH, constants::VALUE_INDICATOR.size()) ==
166 constants::VALUE_INDICATOR) {
169 if (record.size() > constants::ESO_HIERARCH_PREFIX.size()) {
171 auto kw = record.substr(0u, constants::ESO_HIERARCH_PREFIX.size());
172 auto remainder = record.substr(constants::ESO_HIERARCH_PREFIX.size(), record.npos);
173 if (kw == constants::ESO_HIERARCH_PREFIX && remainder.find(
'=') != kw.npos) {
196 constexpr std::string_view ParseLogicalCategory(std::string_view name) noexcept {
197 using namespace std::literals::string_view_literals;
199 auto end = name.find_first_of(
" ="sv);
201 auto full_category = name.substr(0, end);
202 auto logical_end = full_category.find_last_not_of(
"0123456789"sv);
203 if (logical_end != std::string_view::npos) {
204 auto view = name.substr(0, logical_end + 1);
207 return full_category;
210 enum class TrimType { Name, Full };
217 constexpr std::string_view TrimBlanks(std::string_view str, TrimType trim) noexcept {
222 auto trim_pos = str.find_first_not_of(constants::BLANK_CHARS);
223 if (trim_pos == std::string_view::npos) {
225 return trim == TrimType::Name ? str.substr(0u, 1u) : std::string_view();
227 str.remove_prefix(trim_pos);
230 auto trim_pos = str.find_last_not_of(constants::BLANK_CHARS);
231 if (trim_pos != std::string_view::npos) {
232 str.remove_suffix(str.size() - trim_pos - 1);
244 constexpr std::pair<std::string_view, std::optional<std::string_view::size_type>>
245 NextToken(std::string_view str,
char delimiter, TrimType trim) noexcept {
246 auto delimiter_pos = str.find(delimiter);
247 std::optional<std::string_view::size_type> ret_pos;
248 if (delimiter_pos != std::string_view::npos) {
250 ret_pos = delimiter_pos + 1;
251 str.remove_suffix(str.size() - delimiter_pos);
253 auto trimmed = TrimBlanks(str, trim);
254 return {trimmed, ret_pos};
257 constexpr LiteralKeyword::Components
258 ParseValueKeywordComponents(std::array<char, 80>
const& record_array) noexcept {
259 auto record = std::string_view(&record_array[0], record_array.size());
260 LiteralKeyword::Components components{};
264 components.name = TrimBlanks(record.substr(0u, constants::KEYWORD_NAME_LENGTH), TrimType::Name);
265 record.remove_prefix(constants::KEYWORD_NAME_LENGTH + constants::VALUE_INDICATOR.size());
267 auto [value, comment_start] = NextToken(record,
'/', TrimType::Full);
268 components.value = value;
270 if (comment_start.has_value()) {
271 record.remove_prefix(*comment_start);
272 components.comment = TrimBlanks(record, TrimType::Full);
274 components.comment = std::string_view();
280 constexpr LiteralKeyword::Components
281 ParseEsoKeywordComponents(std::array<char, 80>
const& record_array) {
282 auto record = std::string_view(&record_array[0], record_array.size());
283 LiteralKeyword::Components components{};
286 record.remove_prefix(constants::ESO_HIERARCH_PREFIX.size());
288 auto [name, value_start] = NextToken(record,
'=', TrimType::Name);
289 components.name = name;
291 if (!value_start.has_value()) {
292 throw std::invalid_argument(
293 fmt::format(
"Invalid ESO HIERARCH keyword. No '=' value indicator found: {}", record));
295 record.remove_prefix(*value_start);
296 auto [value, comment_start] = NextToken(record,
'/', TrimType::Full);
298 throw std::invalid_argument(
299 fmt::format(
"Invalid ESO HIERARCH keyword. No value found: {}", record));
301 components.value = value;
303 if (comment_start.has_value()) {
304 record.remove_prefix(*comment_start);
305 components.comment = TrimBlanks(record, TrimType::Full);
307 components.comment = std::string_view();
313 constexpr LiteralKeyword::Components
314 ParseCommentaryKeywordComponents(std::array<char, 80>
const& record_array) {
315 auto record = std::string_view(&record_array[0], record_array.size());
316 LiteralKeyword::Components components{};
320 components.name = TrimBlanks(record.substr(0u, constants::KEYWORD_NAME_LENGTH), TrimType::Name);
321 record.remove_prefix(constants::KEYWORD_NAME_LENGTH);
323 components.value = TrimBlanks(record, TrimType::Full);
326 components.comment = std::string_view();
331 [[nodiscard]] constexpr LiteralKeyword::Components
332 ParseKeywordComponents(std::array<char, 80>
const& record) {
333 auto sv_record = std::string_view(&record[0], record.size());
334 auto type = ParseKeywordType(sv_record);
339 return ParseValueKeywordComponents(record);
341 return ParseEsoKeywordComponents(record);
343 return ParseCommentaryKeywordComponents(record);
345 std::cerr << __PRETTY_FUNCTION__ <<
": Invalid type: " << type << std::endl;
352 return lhs.name < rhs.name;
356 return lhs.type == rhs.type && lhs.name == rhs.name;
360 return !(lhs == rhs);
364 std::fill(std::begin(m_record), std::end(m_record),
' ');
366 m_components = ParseKeywordComponents(m_record);
370 : m_record(record), m_components(ParseKeywordComponents(m_record)) {
372 ValidateKeywordName(
GetName());
374 std::throw_with_nested(std::invalid_argument(
"Failed to construct LiteralKeyword"));
380 if (record.size() > constants::RECORD_LENGTH) {
381 throw std::invalid_argument(
382 "LiteralKeyword: Keyword with record > 80 chars is invalid");
384 std::fill(std::begin(m_record), std::end(m_record),
' ');
385 std::copy(std::begin(record), std::end(record), std::begin(m_record));
386 m_components = ParseKeywordComponents(m_record);
387 ValidateKeywordName(
GetName());
389 std::throw_with_nested(std::invalid_argument(
"Failed to construct LiteralKeyword"));
399 m_record = other.m_record;
401 auto make_offset = [&](std::string_view sv) -> std::string_view {
403 return std::string_view();
405 auto start_offset = sv.data() - other.m_record.data();
406 return std::string_view(m_record.data() + start_offset, sv.size());
408 m_components.name = make_offset(other.m_components.name);
409 m_components.value = make_offset(other.m_components.value);
410 m_components.comment = make_offset(other.m_components.comment);
411 m_components.type = other.m_components.type;
416 auto full = std::string_view(&m_record[0], m_record.size());
417 return TrimBlanks(full, TrimType::Name);
421 return lhs.GetName() < rhs.GetName();
425 return lhs.GetRecord() == rhs.GetRecord();
429 return lhs.GetRecord() != rhs.GetRecord();
438 template struct BasicKeyword<ValueKeywordTraits>;
439 template struct BasicKeyword<EsoKeywordTraits>;
441 template std::ostream&
444 template std::ostream&
447 template <
class Trait>
449 char const* string_value,
450 std::optional<std::string> comment_arg)
451 : name(std::move(name_arg)), value(std::string(string_value)), comment(std::move(comment_arg)) {
452 ValidateKeywordName(
GetName());
455 template <
class Trait>
458 std::optional<std::string> comment_arg)
459 : name(std::move(name_arg)), value(std::move(value_arg)), comment(std::move(comment_arg)) {
460 ValidateKeywordName(
GetName());
463 template <
class Trait>
465 return name == rhs.name && value == rhs.value && comment == rhs.comment;
468 template <
class Trait>
470 return !(*
this == rhs);
473 template <
class Trait>
475 return name < rhs.name;
479 if (lhs.index() != rhs.index()) {
483 [&](
auto const& lhs,
auto const& rhs) noexcept ->
bool {
484 return lhs.GetName() == rhs.GetName();
491 if (name.size() > FLEN_KEYWORD) {
492 throw std::invalid_argument(
"Keyword too long");
494 char record[FLEN_CARD] = {
' '};
495 std::copy(name.begin(), name.end(), &record[0]);
497 return static_cast<KeywordClass>(fits_get_keyclass(record));
501 return lhs.GetName() < rhs.GetName();
505 return lhs.GetName() < rhs.GetName();
508 return lhs.GetName() < rhs.GetName();
512 return lhs.GetName() < rhs.GetName();
516 return lhs.GetName() < rhs.GetName();
519 return lhs.GetName() < rhs.GetName();
524 [&](
auto const& var)
mutable {
525 using T = std::decay_t<decltype(var)>;
526 if constexpr (std::is_same_v<T, std::string>) {
527 os <<
"(str)'" << var <<
"'";
528 }
else if constexpr (std::is_same_v<T, bool>) {
529 os <<
"(bool)" << (var ?
"true" :
"false");
531 os <<
"(" << TypeStr<T>() <<
")" << var;
538 template <
class Trait>
540 os <<
"name='" << kw.
name <<
"', value=" << kw.
value <<
", comment=";
542 os <<
"'" << *kw.
comment <<
"'";
550 std::visit([&](
auto const& var)
mutable { os << var; }, kw);
555 for (
auto const& kw : from) {
556 if (
auto it = std::find_if(
557 to.begin(), to.end(), [&](
auto const& val) { return NameEquals(val, kw); });
571 KeywordVector::iterator position,
572 KeywordVector::const_iterator from_first,
573 KeywordVector::const_iterator from_last) {
576 auto num_elements = std::distance(from_first, from_last);
577 auto pred = [&](KeywordVector::value_type
const& kw) ->
bool {
578 return std::find_if(from_first, from_last, [&](
auto const& val) {
582 auto inserted_first = keywords.insert(position, from_first, from_last);
583 auto after_inserted_first = inserted_first;
584 std::advance(after_inserted_first, num_elements);
586 keywords.erase(std::remove_if(after_inserted_first, keywords.end(), pred), keywords.end());
589 keywords.erase(std::remove_if(keywords.begin(), inserted_first, pred), inserted_first);
595 auto value = FormatFitsValue(name, keyword.
value);
596 auto comment = keyword.
comment.value_or(
"");
597 return InternalEsoKeywordFormat(name.name, value, comment);
603 auto value = FormatFitsValue(name, keyword.
value);
604 auto comment = keyword.
comment.value_or(
"");
605 return InternalValueKeywordFormat(
606 name.name, value, comment, std::holds_alternative<std::string>(keyword.
value));
610 switch (keyword.
type) {
612 bool is_str = keyword.
value.find(
'\'') != std::string_view::npos;
613 return InternalValueKeywordFormat(keyword.
name, keyword.
value, keyword.
comment, is_str);
616 return InternalEsoKeywordFormat(keyword.
name, keyword.
value, keyword.
comment);
619 return InternalCommentaryKeywordFormat(keyword.
name, keyword.
value);
622 std::cerr << __PRETTY_FUNCTION__ <<
": Invalid type: " << keyword.
type << std::endl;
629 using T = std::decay_t<decltype(kw)>;
630 if constexpr (std::is_same_v<T, LiteralKeyword>) {
631 return Format(kw.GetComponents());
641 constexpr
bool SortEsoKeywordName(std::string_view lhs, std::string_view rhs) noexcept {
642 using namespace std::literals::string_view_literals;
644 auto lhs_cat = ParseLogicalCategory(lhs);
645 auto rhs_cat = ParseLogicalCategory(rhs);
646 if (lhs_cat == rhs_cat) {
651 std::array<std::string_view, 8> categories = {
652 "DPR"sv,
"OBS"sv,
"TPL"sv,
"GEN"sv,
"TEL"sv,
"ADA"sv,
"INS"sv,
"DET"sv};
653 auto lhs_idx = std::find(std::begin(categories), std::end(categories), lhs_cat);
654 auto rhs_idx = std::find(std::begin(categories), std::end(categories), rhs_cat);
656 if (lhs_idx != std::end(categories)) {
657 if (rhs_idx != std::end(categories)) {
660 return std::distance(lhs_idx, rhs_idx) > 0;
666 if (rhs_idx != std::end(categories)) {
678 auto lhs = lhs_kw.GetName();
679 auto rhs = rhs_kw.GetName();
700 return SortEsoKeywordName(lhs.name, rhs.name);
714 return lhs.name < rhs.name;
721 std::stable_sort(std::begin(keywords), std::end(keywords),
StandardLess{});
726 #if defined(UNIT_TEST)
730 static_assert(NextToken(
" ='date'",
'=', TrimType::Name) ==
731 std::pair<std::string_view, std::optional<std::string_view::size_type>>(
" ", 9));
Represents the literal 80-character FITS keyword record.
std::string_view comment
Comment may be empty.
LiteralKeyword & operator=(LiteralKeyword const &other) noexcept
std::string_view GetRecord() const &noexcept
constexpr KeywordNameView GetName() const &noexcept
Query logical keyword name.
LiteralKeyword() noexcept
Initializes an empty record (filled with ' ' characters)
Decomposed components a literal keyword.
Contains data structure for FITS keywords.
void StandardSort(std::vector< LiteralKeyword > &keywords)
Sorts keywords according to ESO DICD standards.
void InsertKeywords(KeywordVector &keywords, KeywordVector::iterator position, KeywordVector::const_iterator from_first, KeywordVector::const_iterator from_last)
Insert keywords.
KeywordType
Type of FITS keyword.
@ Eso
An ESO hiearchical keyword.
@ Commentary
A commentary keyword, which are keywords that do not fall into the previous categories.
bool NameEquals(KeywordVariant const &lhs, KeywordVariant const &rhs) noexcept
Compare logical keyword names of keyword of the same type.
bool operator==(KeywordNameView lhs, KeywordNameView rhs) noexcept
LiteralKeyword Format(KeywordVariant const &keyword)
bool operator<(LiteralKeyword const &, LiteralKeyword const &) noexcept
Sort by logical keyword name (not DICD sort)
template std::ostream & operator<<< ValueKeywordTraits >(std::ostream &os, BasicKeyword< ValueKeywordTraits > const &kw)
KeywordClass GetKeywordClass(std::string_view name)
Get keyword class.
KeywordClass
Fits keyword type.
std::vector< KeywordVariant > KeywordVector
Vector of keywords.
bool operator!=(KeywordNameView lhs, KeywordNameView rhs) noexcept
std::variant< ValueKeyword, EsoKeyword, LiteralKeyword > KeywordVariant
The different variants of keywords that are supported.
void UpdateKeywords(KeywordVector &to, KeywordVector const &from, ConflictPolicy policy=ConflictPolicy::Replace)
Updates to with keywords from from.
template std::ostream & operator<<< EsoKeywordTraits >(std::ostream &os, BasicKeyword< EsoKeywordTraits > const &kw)
@ Replace
Replace keyword that conflicts.
std::ostream & operator<<(std::ostream &os, LiteralKeyword const &kw)
std::variant< std::string, int64_t, uint64_t, double, bool > ValueType
A type safe version of LiteralKeyword that consist of the three basic components of a FITS keyword ke...
bool operator==(BasicKeyword const &rhs) const noexcept
Compares all members for equality.
bool operator!=(BasicKeyword const &rhs) const noexcept
Compares all members for inequality.
bool operator<(BasicKeyword const &rhs) const noexcept
Uses name property as the sorting index.
std::string name
Trimmed keyword name.
std::optional< std::string > comment
Trimmed keyword comment.
constexpr KeywordNameView GetName() const &noexcept
Query logical keyword name.