15#include <boost/algorithm/string/join.hpp>
16#include <boost/assert.hpp>
17#include <boost/exception/enable_current_exception.hpp>
18#include <fmt/format.h>
23inline constexpr bool always_false_v =
false;
25char const* TypeStr() noexcept;
27char const* TypeStr<std::uint64_t>() noexcept {
31char const* TypeStr<std::int64_t>() noexcept {
35char const* TypeStr<double>() noexcept {
39enum class ParseResult {
51std::string FitsEscapeString(std::string_view value) {
52 std::string new_value;
53 new_value.reserve(value.size());
54 for (
auto c : value) {
55 new_value.push_back(c);
57 new_value.push_back(c);
63constexpr char const* FMT_VALUEKW_SCALAR =
"{:<8}= {:>20}";
64constexpr char const* FMT_VALUEKW_STR =
"{:<8}= {:<20}";
65constexpr char const* FMT_ESOKW =
"HIERARCH ESO {} = {}";
69constexpr char const* FMT_ESOKW_PACKED =
"HIERARCH ESO {}= {}";
70constexpr char const* FMT_ALL_COMMENT =
"{} / {}";
71constexpr char const* FMT_COMMENTARYKW =
"{:<8} {}";
73LiteralKeyword InternalValueKeywordFormat(std::string_view name,
74 std::string_view value,
75 std::string_view comment) {
76 auto is_string = value.find(
'\'') == 0u;
78 auto formatted_name_value =
79 fmt::format(is_string ? FMT_VALUEKW_STR : FMT_VALUEKW_SCALAR, name, value);
80 if (formatted_name_value.size() > constants::RECORD_LENGTH) {
81 throw boost::enable_current_exception(InvalidKeyword(
83 fmt::format(
"formatted name and value does not fit in keyword record. Length: {}, "
84 "Formatted result: '{}'",
85 formatted_name_value.size(),
86 formatted_name_value)));
88 auto formatted = fmt::format(FMT_ALL_COMMENT, formatted_name_value, comment);
89 auto view = std::string_view(formatted).substr(0u, fits::constants::RECORD_LENGTH);
90 return LiteralKeyword(view);
94InternalEsoKeywordFormat(std::string_view name, std::string_view value, std::string_view comment) {
95 const bool use_packed_value =
96 name.size() + value.size() >= constants::ESO_HIERARCH_NAME_VALUE_PACKED_LIMIT;
97 auto formatted_name_value =
98 fmt::format(use_packed_value ? FMT_ESOKW_PACKED : FMT_ESOKW, name, value);
99 if (formatted_name_value.size() > constants::RECORD_LENGTH) {
100 throw boost::enable_current_exception(InvalidKeyword(
102 fmt::format(
"formatted name and value does not fit in keyword record. Length: {}, "
103 "Formatted result: '{}'",
104 formatted_name_value.size(),
105 formatted_name_value)));
107 auto formatted = fmt::format(FMT_ALL_COMMENT, formatted_name_value, comment);
108 auto view = std::string_view(formatted).substr(0u, fits::constants::RECORD_LENGTH);
109 return LiteralKeyword(view);
112LiteralKeyword InternalCommentaryKeywordFormat(std::string_view name, std::string_view value) {
113 auto formatted_name_value = fmt::format(FMT_COMMENTARYKW, name, value);
114 if (formatted_name_value.size() > constants::RECORD_LENGTH) {
115 throw boost::enable_current_exception(InvalidKeyword(
117 fmt::format(
"formatted name and value does not fit in keyword record. Length: {}, "
118 "Formatted result: '{}'",
119 formatted_name_value.size(),
120 formatted_name_value)));
122 return LiteralKeyword(formatted_name_value);
143void ValidateKeywordName(KeywordNameView keyword) {
144 if (keyword.name.empty()) {
145 throw boost::enable_current_exception(InvalidKeyword(
"",
"keyword name cannot be empty"));
147 auto max_length = keyword.type ==
KeywordType::Eso ? constants::ESO_HIERARCH_MAX_NAME_LENGTH
148 : constants::KEYWORD_NAME_LENGTH;
149 if (keyword.name.size() > max_length) {
150 auto msg = fmt::format(
"keyword name too long. Maximum is {} and provided name '{}' is {}",
153 keyword.name.size());
154 throw boost::enable_current_exception(InvalidKeyword(keyword.name, msg));
156 auto is_valid_char = [](
char c) ->
bool {
157 return (c >=
'0' && c <=
'9') || (c >=
'A' && c <=
'Z') || c ==
' ' || c ==
'_' || c ==
'-';
160 if (!std::all_of(std::begin(keyword.name), std::end(keyword.name), is_valid_char)) {
161 throw boost::enable_current_exception(InvalidKeyword(
164 "Keyword name '{}' contains illegal character. Valid charset is [A-Z0-9_-].",
169constexpr KeywordType ParseKeywordType(std::string_view record)
noexcept {
170 if (record.size() >= constants::KEYWORD_NAME_LENGTH + constants::VALUE_INDICATOR.size()) {
172 if (record.substr(constants::KEYWORD_NAME_LENGTH, constants::VALUE_INDICATOR.size()) ==
173 constants::VALUE_INDICATOR) {
176 if (record.size() > constants::ESO_HIERARCH_PREFIX.size()) {
178 auto kw = record.substr(0u, constants::ESO_HIERARCH_PREFIX.size());
179 auto remainder = record.substr(constants::ESO_HIERARCH_PREFIX.size(), record.npos);
180 if (kw == constants::ESO_HIERARCH_PREFIX && remainder.find(
'=') != kw.npos) {
203constexpr std::string_view ParseLogicalCategory(std::string_view name)
noexcept {
204 using namespace std::literals::string_view_literals;
206 auto end = name.find_first_of(
" ="sv);
208 auto full_category = name.substr(0, end);
209 auto logical_end = full_category.find_last_not_of(
"0123456789"sv);
210 if (logical_end != std::string_view::npos) {
211 auto view = name.substr(0, logical_end + 1);
214 return full_category;
217enum class TrimType { Name, Full };
219constexpr std::string_view TrimLeft(std::string_view str, TrimType trim) {
220 auto trim_pos = str.find_first_not_of(constants::BLANK_CHARS);
221 if (trim_pos == std::string_view::npos) {
223 return trim == TrimType::Name ? str.substr(0u, 1u) : std::string_view();
225 str.remove_prefix(trim_pos);
234constexpr std::string_view TrimBlanks(std::string_view str, TrimType trim)
noexcept {
238 str = TrimLeft(str, trim);
240 auto trim_pos = str.find_last_not_of(constants::BLANK_CHARS);
241 if (trim_pos != std::string_view::npos) {
242 str.remove_suffix(str.size() - trim_pos - 1);
251constexpr std::string_view NextStringToken(std::string_view str, ParseResult& ec) {
252 ec = ParseResult::Ok;
253 str = TrimLeft(str, TrimType::Full);
257 if (str[0] !=
'\'') {
258 ec = ParseResult::NotAString;
262 auto str_end = std::string_view::npos;
263 bool possible_end =
false;
265 auto break_chars = std::string_view(
" /");
266 for (str_end = 1; str_end < str.size(); ++str_end) {
267 if (possible_end && break_chars.find(str[str_end]) != std::string_view::npos) {
269 }
else if (possible_end && str[str_end] ==
'\'') {
271 possible_end =
false;
272 }
else if (!possible_end && str[str_end] ==
'\'') {
278 ec = ParseResult::InvalidString;
281 return str.substr(0, str_end);
291constexpr std::pair<std::string_view, std::optional<std::string_view::size_type>>
292NextToken(std::string_view str,
char delimiter, TrimType trim)
noexcept {
293 auto delimiter_pos = str.find(delimiter);
294 std::optional<std::string_view::size_type> ret_pos;
295 if (delimiter_pos != std::string_view::npos) {
297 ret_pos = delimiter_pos + 1;
298 str.remove_suffix(str.size() - delimiter_pos);
300 auto trimmed = TrimBlanks(str, trim);
301 return {trimmed, ret_pos};
315constexpr std::pair<std::string_view, std::optional<std::string_view::size_type>>
316NextValue(std::string_view str, ParseResult& ec)
noexcept {
318 auto value = NextStringToken(str, ec);
319 if (ec == ParseResult::InvalidString) {
324 if (!value.empty()) {
326 offset = value.end() - str.begin();
328 str.remove_prefix(offset);
330 ec = ParseResult::Ok;
333 auto [alt, end] = NextToken(str,
'/', TrimType::Full);
334 auto comment_beg = end.has_value() ? *end - 1 : std::string_view::npos;
341 if (value.find_first_of(constants::BLANK_CHARS) != std::string_view::npos) {
343 ec = ParseResult::ExtraChars;
347 auto shrink_by = value.end() - str.begin();
348 str.remove_prefix(shrink_by);
349 if (comment_beg != std::string_view::npos) {
350 comment_beg -= shrink_by;
355 auto empty = str.substr(0, comment_beg);
356 if (empty.find_first_not_of(constants::BLANK_CHARS) != std::string_view::npos) {
357 ec = ParseResult::ExtraChars;
361 return {value, *end + offset};
363 return {value, std::nullopt};
366constexpr LiteralKeyword::Components
367ParseValueKeywordComponents(std::array<char, 80>
const& record_array) {
368 auto record = std::string_view(&record_array[0], record_array.size());
369 LiteralKeyword::Components components{};
373 components.name = TrimBlanks(record.substr(0u, constants::KEYWORD_NAME_LENGTH), TrimType::Name);
374 record.remove_prefix(constants::KEYWORD_NAME_LENGTH + constants::VALUE_INDICATOR.size());
376 ParseResult res = ParseResult::Ok;
377 auto [value, comment_start] = NextValue(record, res);
378 if (res != ParseResult::Ok) {
379 throw boost::enable_current_exception(InvalidKeyword(
380 components.name, fmt::format(
"Invalid keyword. Invalid value found: {}", record)));
382 components.value = value;
385 if (comment_start.has_value()) {
386 record.remove_prefix(*comment_start);
387 components.comment = TrimBlanks(record, TrimType::Full);
389 components.comment = std::string_view();
395constexpr LiteralKeyword::Components
396ParseEsoKeywordComponents(std::array<char, 80>
const& record_array) {
397 auto record = std::string_view(&record_array[0], record_array.size());
398 LiteralKeyword::Components components{};
401 record.remove_prefix(constants::ESO_HIERARCH_PREFIX.size());
403 auto [name, value_start] = NextToken(record,
'=', TrimType::Name);
404 components.name = name;
406 if (!value_start.has_value()) {
407 throw boost::enable_current_exception(InvalidKeyword(
409 fmt::format(
"invalid ESO HIERARCH keyword. No '=' value indicator found: {}", record)));
411 record.remove_prefix(*value_start);
412 ParseResult res = ParseResult::Ok;
413 auto [value, comment_start] = NextValue(record, res);
414 if (res != ParseResult::Ok) {
415 throw boost::enable_current_exception(InvalidKeyword(
416 name, fmt::format(
"Invalid ESO HIERARCH keyword. Invalid value found: {}", record)));
418 components.value = value;
420 if (comment_start.has_value()) {
421 record.remove_prefix(*comment_start);
422 components.comment = TrimBlanks(record, TrimType::Full);
424 components.comment = std::string_view();
430constexpr LiteralKeyword::Components
431ParseCommentaryKeywordComponents(std::array<char, 80>
const& record_array) {
432 auto record = std::string_view(&record_array[0], record_array.size());
433 LiteralKeyword::Components components{};
437 components.name = TrimBlanks(record.substr(0u, constants::KEYWORD_NAME_LENGTH), TrimType::Name);
438 record.remove_prefix(constants::KEYWORD_NAME_LENGTH);
440 components.value = TrimBlanks(record, TrimType::Full);
443 components.comment = std::string_view();
448[[nodiscard]]
constexpr LiteralKeyword::Components
449ParseKeywordComponents(std::array<char, 80>
const& record) {
450 auto sv_record = std::string_view(&record[0], record.size());
451 auto type = ParseKeywordType(sv_record);
456 return ParseValueKeywordComponents(record);
458 return ParseEsoKeywordComponents(record);
460 return ParseCommentaryKeywordComponents(record);
462 std::cerr << __PRETTY_FUNCTION__ <<
": Invalid type: " << type << std::endl;
469 return lhs.name < rhs.name;
473 return lhs.type == rhs.type && lhs.name == rhs.name;
477 return !(lhs == rhs);
481 std::fill(std::begin(m_record), std::end(m_record),
' ');
483 m_components = ParseKeywordComponents(m_record);
487 : m_record(record), m_components(ParseKeywordComponents(m_record)) {
489 ValidateKeywordName(
GetName());
491 std::throw_with_nested(std::invalid_argument(
"Failed to construct LiteralKeyword"));
497 if (record.size() > constants::RECORD_LENGTH) {
499 record,
"literalKeyword: Keyword with record > 80 chars is invalid"));
501 std::fill(std::begin(m_record), std::end(m_record),
' ');
502 std::copy(std::begin(record), std::end(record), std::begin(m_record));
503 m_components = ParseKeywordComponents(m_record);
504 ValidateKeywordName(
GetName());
506 std::throw_with_nested(std::invalid_argument(
"Failed to construct LiteralKeyword"));
516 m_record = other.m_record;
518 auto make_offset = [&](std::string_view sv) -> std::string_view {
520 return std::string_view();
522 auto start_offset = sv.data() - other.m_record.data();
523 return std::string_view(m_record.data() + start_offset, sv.size());
525 m_components.name = make_offset(other.m_components.name);
526 m_components.value = make_offset(other.m_components.value);
527 m_components.comment = make_offset(other.m_components.comment);
528 m_components.type = other.m_components.type;
533 auto full = std::string_view(&m_record[0], m_record.size());
534 return TrimBlanks(full, TrimType::Name);
538 return lhs.GetName() < rhs.GetName();
542 return lhs.GetRecord() == rhs.GetRecord();
546 return lhs.GetRecord() != rhs.GetRecord();
556 return lhs.type == rhs.type && lhs.name == rhs.name && lhs.value == rhs.value &&
557 lhs.comment == rhs.comment;
569template std::ostream&
572template std::ostream&
575template <
class Trait>
577 char const* string_value,
578 std::optional<std::string> comment_arg)
579 : name(std::move(name_arg)), value(std::string(string_value)), comment(std::move(comment_arg)) {
580 ValidateKeywordName(
GetName());
583template <
class Trait>
586 std::optional<std::string> comment_arg)
587 : name(std::move(name_arg)), value(std::move(value_arg)), comment(std::move(comment_arg)) {
588 ValidateKeywordName(
GetName());
591template <
class Trait>
593 return name == rhs.name && value == rhs.value && comment == rhs.comment;
596template <
class Trait>
598 return !(*
this == rhs);
601template <
class Trait>
603 return name < rhs.name;
608 [&](
auto const& lhs,
auto const& rhs)
noexcept ->
bool {
609 return lhs.GetName() == rhs.GetName();
616 if (name.size() > FLEN_KEYWORD) {
617 throw boost::enable_current_exception(
InvalidKeyword(name,
"keyword too long"));
619 char record[FLEN_CARD] = {
' '};
620 std::copy(name.begin(), name.end(), &record[0]);
622 return static_cast<KeywordClass>(fits_get_keyclass(record));
626 return lhs.GetName() < rhs.GetName();
630 return lhs.GetName() < rhs.GetName();
633 return lhs.GetName() < rhs.GetName();
637 return lhs.GetName() < rhs.GetName();
641 return lhs.GetName() < rhs.GetName();
644 return lhs.GetName() < rhs.GetName();
649 [&](
auto const& var)
mutable {
650 using T = std::decay_t<
decltype(var)>;
651 if constexpr (std::is_same_v<T, std::string>) {
652 os <<
"(str)'" << var <<
"'";
653 }
else if constexpr (std::is_same_v<T, bool>) {
654 os <<
"(bool)" << (var ?
"true" :
"false");
656 os <<
"(" << TypeStr<T>() <<
")" << var;
663template <
class Trait>
665 os <<
"name='" << kw.
name <<
"', value=" << kw.
value <<
", comment=";
667 os <<
"'" << *kw.
comment <<
"'";
675 std::visit([&](
auto const& var)
mutable { os << var; }, kw);
680 for (
auto const& kw : from) {
681 if (
auto it = std::find_if(
682 to.begin(), to.end(), [&](
auto const& val) { return NameEquals(val, kw); });
696 KeywordVector::iterator position,
697 KeywordVector::const_iterator from_first,
698 KeywordVector::const_iterator from_last) {
701 auto num_elements = std::distance(from_first, from_last);
702 auto pred = [&](KeywordVector::value_type
const& kw) ->
bool {
703 return std::find_if(from_first, from_last, [&](
auto const& val) {
707 auto inserted_first = keywords.insert(position, from_first, from_last);
708 auto after_inserted_first = inserted_first;
709 std::advance(after_inserted_first, num_elements);
711 keywords.erase(std::remove_if(after_inserted_first, keywords.end(), pred), keywords.end());
714 keywords.erase(std::remove_if(keywords.begin(), inserted_first, pred), inserted_first);
718 using namespace std::string_view_literals;
720 [&](
auto& value) -> std::string {
721 using T = std::decay_t<
decltype(value)>;
722 if constexpr (std::is_same_v<T, std::string>) {
724 auto escaped_value = FitsEscapeString(value);
727 return fmt::format(
"'{: <8s}'", escaped_value);
729 return fmt::format(
"'{}'", escaped_value);
731 }
else if constexpr (std::is_same_v<T, bool>) {
732 return value ?
"T" :
"F";
733 }
else if constexpr (std::is_same_v<T, double>) {
735 return fmt::format(
"{:g}", value);
737 return fmt::format(
"{:.1f}", value);
739 }
else if constexpr (std::is_same_v<T, std::int64_t>) {
740 return fmt::format(
"{:d}", value);
741 }
else if constexpr (std::is_same_v<T, std::uint64_t>) {
742 return fmt::format(
"{:d}", value);
744 static_assert(always_false_v<T>,
"non-exhaustive visitor!");
754 auto comment = keyword.
comment.value_or(
"");
755 return InternalEsoKeywordFormat(name.name, value, comment);
762 auto comment = keyword.
comment.value_or(
"");
763 return InternalValueKeywordFormat(name.name, value, comment);
767 switch (keyword.
type) {
769 return InternalValueKeywordFormat(keyword.
name, keyword.
value, keyword.
comment);
772 return InternalEsoKeywordFormat(keyword.
name, keyword.
value, keyword.
comment);
775 return InternalCommentaryKeywordFormat(keyword.
name, keyword.
value);
778 std::cerr << __PRETTY_FUNCTION__ <<
": Invalid type: " << keyword.
type << std::endl;
785 using T = std::decay_t<
decltype(kw)>;
786 if constexpr (std::is_same_v<T, LiteralKeyword>) {
787 return Format(kw.GetComponents());
800 : std::invalid_argument(fmt::format(
"Invalid keyword `{}` {}", name, what)) {
805 fmt::format(
"keyword not found in any of the dictionaries: {}",
806 boost::algorithm::join(dictionaries,
", "))) {
820 return InternalValueKeywordFormat(name.
name, value, comment);
822 return InternalEsoKeywordFormat(name.
name, value, comment);
824 BOOST_ASSERT_MSG(
false,
"Invalid keyword type");
830constexpr bool SortEsoKeywordName(std::string_view lhs, std::string_view rhs)
noexcept {
831 using namespace std::literals::string_view_literals;
833 auto lhs_cat = ParseLogicalCategory(lhs);
834 auto rhs_cat = ParseLogicalCategory(rhs);
835 if (lhs_cat == rhs_cat) {
840 std::array<std::string_view, 8> categories = {
841 "DPR"sv,
"OBS"sv,
"TPL"sv,
"GEN"sv,
"TEL"sv,
"ADA"sv,
"INS"sv,
"DET"sv};
842 auto lhs_idx = std::find(std::begin(categories), std::end(categories), lhs_cat);
843 auto rhs_idx = std::find(std::begin(categories), std::end(categories), rhs_cat);
845 if (lhs_idx != std::end(categories)) {
846 if (rhs_idx != std::end(categories)) {
849 return std::distance(lhs_idx, rhs_idx) > 0;
855 if (rhs_idx != std::end(categories)) {
867 auto lhs = lhs_kw.GetName();
868 auto rhs = rhs_kw.GetName();
889 return SortEsoKeywordName(lhs.name, rhs.name);
903 return lhs.name < rhs.name;
910 std::stable_sort(std::begin(keywords), std::end(keywords),
StandardLess{});
915#if defined(UNIT_TEST)
919static_assert(NextToken(
" ='date'",
'=', TrimType::Name) ==
920 std::pair<std::string_view, std::optional<std::string_view::size_type>>(
" ", 9));
Indicates keyword is invalid for some reason.
InvalidKeyword(std::string_view name, char const *reason)
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.
UnknownKeyword(std::string_view kw, std::vector< std::string > const &dictionaries)
Contains data structure for FITS keywords.
void StandardSort(std::vector< LiteralKeyword > &keywords)
Sorts keywords according to ESO DICD standards.
template std::ostream & operator<<< ValueKeywordTraits >(std::ostream &os, BasicKeyword< ValueKeywordTraits > const &kw)
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)
KeywordClass GetKeywordClass(std::string_view name)
Get keyword class.
auto FormatFitsValue(KeywordNameView name, BasicKeywordBase::ValueType const &value) -> std::string
Format keyword value using built-in rules such that it can be used as a component when formatting a c...
auto UntypedFormat(KeywordNameView name, std::string_view value, std::string_view comment) -> LiteralKeyword
Untyped formatting where value already has been formatted correctly.
KeywordClass
Fits keyword type.
template std::ostream & operator<<< EsoKeywordTraits >(std::ostream &os, BasicKeyword< EsoKeywordTraits > const &kw)
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.
std::ostream & operator<<(std::ostream &os, HduType hdu_type)
Format HduType hdu_type to os.
@ Replace
Replace keyword that conflicts.
std::variant< std::string, std::int64_t, std::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.