ifw-daq 3.1.0
IFW Data Acquisition modules
Loading...
Searching...
No Matches
keyword.cpp
Go to the documentation of this file.
1/**
2 * @file
3 * @ingroup daq_ocm_libfits
4 * @copyright 2022 ESO - European Southern Observatory
5 *
6 * @brief Definition of contents from fits/keyword.hpp
7 */
9
10#include <algorithm>
11#include <cstdlib>
12#include <iostream>
13#include <ostream>
14
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>
19
20namespace daq::fits {
21namespace {
22template <class>
23inline constexpr bool always_false_v = false; // NOLINT
24template <class T>
25char const* TypeStr() noexcept;
26template <>
27char const* TypeStr<std::uint64_t>() noexcept {
28 return "uint64_t";
29}
30template <>
31char const* TypeStr<std::int64_t>() noexcept {
32 return "int64_t";
33}
34template <>
35char const* TypeStr<double>() noexcept {
36 return "double";
37}
38
39enum class ParseResult {
40 Ok,
41 // There are left-over characters after parsing.
42 ExtraChars,
43 // Not a string
44 NotAString,
45 // A string but invalid
46 InvalidString,
47};
48/**
49 * Escape string "foo's" -> "foo''s"
50 */
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);
56 if (c == '\'') {
57 new_value.push_back(c);
58 }
59 }
60 return new_value;
61}
62
63constexpr char const* FMT_VALUEKW_SCALAR = "{:<8}= {:>20}";
64constexpr char const* FMT_VALUEKW_STR = "{:<8}= {:<20}";
65constexpr char const* FMT_ESOKW = "HIERARCH ESO {} = {}";
66// HIERARCH keyword standard follows FITS free-format specified in standard:
67// - Value indicator is at least `= `
68// - Comment indicator is ` /`
69constexpr char const* FMT_ESOKW_PACKED = "HIERARCH ESO {}= {}";
70constexpr char const* FMT_ALL_COMMENT = "{} / {}";
71constexpr char const* FMT_COMMENTARYKW = "{:<8} {}";
72
73LiteralKeyword InternalValueKeywordFormat(std::string_view name,
74 std::string_view value,
75 std::string_view comment) {
76 auto is_string = value.find('\'') == 0u;
77 // Fixed value format
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(
82 name,
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)));
87 }
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);
91}
92
93LiteralKeyword
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(
101 name,
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)));
106 }
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);
110}
111
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(
116 name,
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)));
121 }
122 return LiteralKeyword(formatted_name_value);
123}
124
125/**
126 * Validates logical keyword name according to FITS rules.
127 * The rules depend on the type of keyword.
128 *
129 * Common rules:
130 *
131 * - Trailing whitespace are insignificant.
132 * - Valid character set: [A-Z0-9_-]
133 *
134 * KeywordType::Value and KeywordType::Commentary:
135 * - Size: 1-8 (for an all-spaces keyword there must be one ' ')
136 *
137 * KeywordType::Eso:
138 *
139 * - Size: 1 - ESO_HIERARCH_MAX_NAME_LENGTH
140 *
141 * @throws std::invalid_argument on errors
142 */
143void ValidateKeywordName(KeywordNameView keyword) {
144 if (keyword.name.empty()) {
145 throw boost::enable_current_exception(InvalidKeyword("", "keyword name cannot be empty"));
146 }
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 {}",
151 max_length,
152 keyword.name,
153 keyword.name.size());
154 throw boost::enable_current_exception(InvalidKeyword(keyword.name, msg));
155 }
156 auto is_valid_char = [](char c) -> bool {
157 return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || c == ' ' || c == '_' || c == '-';
158 };
159
160 if (!std::all_of(std::begin(keyword.name), std::end(keyword.name), is_valid_char)) {
161 throw boost::enable_current_exception(InvalidKeyword(
162 keyword.name,
163 fmt::format(
164 "Keyword name '{}' contains illegal character. Valid charset is [A-Z0-9_-].",
165 keyword.name)));
166 }
167}
168
169constexpr KeywordType ParseKeywordType(std::string_view record) noexcept {
170 if (record.size() >= constants::KEYWORD_NAME_LENGTH + constants::VALUE_INDICATOR.size()) {
171 // If bytes 9 and 10 contain the value indicator it must be a value keyword
172 if (record.substr(constants::KEYWORD_NAME_LENGTH, constants::VALUE_INDICATOR.size()) ==
173 constants::VALUE_INDICATOR) {
174 return KeywordType::Value;
175 }
176 if (record.size() > constants::ESO_HIERARCH_PREFIX.size()) {
177 // HIERARCH ESO keyword must start with 'HIERARCH ESO ' and contain '='.
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) {
181 return KeywordType::Eso;
182 }
183 }
184 }
185 // Everything else are Commentary keywords.
187}
188
189/**
190 * Parse the logical category of an ESO hierarchical keyword.
191 *
192 * Logical category ignores trailing numbers, so logical category of
193 * - `DET1` is `DET`
194 * - `DET01` is `DET`
195 *
196 * However, the logical category for
197 * - `DET1A` is `DET1A`
198 * - `DET1A1` is `DET1A`.
199 *
200 * @param name Logical keyword name with the first token being the category.
201 * @return logical category (first token stripped of trailing numbers)
202 */
203constexpr std::string_view ParseLogicalCategory(std::string_view name) noexcept {
204 using namespace std::literals::string_view_literals;
205 // End of category token
206 auto end = name.find_first_of(" ="sv);
207 // Trim away any trailing numbers
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);
212 return view;
213 }
214 return full_category;
215}
216
217enum class TrimType { Name, Full };
218
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) {
222 // All blanks
223 return trim == TrimType::Name ? str.substr(0u, 1u) : std::string_view();
224 }
225 str.remove_prefix(trim_pos);
226 return str;
227}
228
229/**
230 * Trim string view.
231 * If @a trim is @a Name it will try to keep a single space char in the event of a completely empty
232 * string because that is significant in FITS.
233 */
234constexpr std::string_view TrimBlanks(std::string_view str, TrimType trim) noexcept {
235 if (str.empty()) {
236 return str;
237 }
238 str = TrimLeft(str, trim);
239 { // Trim right
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);
243 }
244 }
245 return str;
246}
247
248/**
249 * @returns FITS string token or empty string view.
250 */
251constexpr std::string_view NextStringToken(std::string_view str, ParseResult& ec) {
252 ec = ParseResult::Ok;
253 str = TrimLeft(str, TrimType::Full);
254 if (str.empty()) {
255 return {};
256 }
257 if (str[0] != '\'') {
258 ec = ParseResult::NotAString;
259 return {};
260 }
261 // one-past end of string
262 auto str_end = std::string_view::npos;
263 bool possible_end = false;
264 // valid characters after closed string `'` are the space or comment characters.
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) {
268 break;
269 } else if (possible_end && str[str_end] == '\'') {
270 // Two consecutive ' -> not a possible end any more
271 possible_end = false;
272 } else if (!possible_end && str[str_end] == '\'') {
273 possible_end = true;
274 }
275 }
276 // Did string end without closing?
277 if (!possible_end) {
278 ec = ParseResult::InvalidString;
279 return {};
280 } else {
281 return str.substr(0, str_end);
282 }
283}
284
285/**
286 * Read next token in str up to @a delimiter.
287 *
288 * @returns Trimmed token and position after delimiter in the input str. If delimiter was not found
289 * the returned string_view iterator will be nullopt.
290 */
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) {
296 // Delimiter found
297 ret_pos = delimiter_pos + 1;
298 str.remove_suffix(str.size() - delimiter_pos);
299 }
300 auto trimmed = TrimBlanks(str, trim);
301 return {trimmed, ret_pos};
302}
303
304/**
305 * Read FITS value from str
306 *
307 * Value is either
308 * - a string: 'string' (string may contain ' which are escaped as '')
309 * - One of the FITS value types.
310 *
311 * Up to an optional comment delimiter `/` which does not require a space before it.
312 *
313 * @returns trimmed value and position of comment delmiter `/`, if any.
314 */
315constexpr std::pair<std::string_view, std::optional<std::string_view::size_type>>
316NextValue(std::string_view str, ParseResult& ec) noexcept {
317 // Try to find a string
318 auto value = NextStringToken(str, ec);
319 if (ec == ParseResult::InvalidString) {
320 return {};
321 }
322 // Offset from local string to input str
323 auto offset = 0u;
324 if (!value.empty()) {
325 // Found string -> remove value
326 offset = value.end() - str.begin();
327 // shrink `str` to exclude value
328 str.remove_prefix(offset);
329 }
330 ec = ParseResult::Ok;
331
332 // Find out where comment begins (and value if not a string)
333 auto [alt, end] = NextToken(str, '/', TrimType::Full);
334 auto comment_beg = end.has_value() ? *end - 1 : std::string_view::npos;
335 if (value.empty()) {
336 if (alt.empty()) {
337 return {{}, end};
338 }
339 // Since value from string was not provided we use value up to `/`
340 value = alt;
341 if (value.find_first_of(constants::BLANK_CHARS) != std::string_view::npos) {
342 // there are blank chars in non-string keyword
343 ec = ParseResult::ExtraChars;
344 return {};
345 }
346 // shrink `str` to exclude value
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;
351 }
352 }
353
354 // `str` now begins after value
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;
358 return {};
359 }
360 if (end) {
361 return {value, *end + offset};
362 }
363 return {value, std::nullopt};
364}
365
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{};
370 components.type = KeywordType::Value;
371
372 // Name
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());
375 // Value
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)));
381 }
382 components.value = value;
383
384 // Optional comment
385 if (comment_start.has_value()) {
386 record.remove_prefix(*comment_start);
387 components.comment = TrimBlanks(record, TrimType::Full);
388 } else {
389 components.comment = std::string_view();
390 }
391
392 return components;
393}
394
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{};
399 components.type = KeywordType::Eso;
400 // Name
401 record.remove_prefix(constants::ESO_HIERARCH_PREFIX.size());
402
403 auto [name, value_start] = NextToken(record, '=', TrimType::Name);
404 components.name = name;
405 // Value
406 if (!value_start.has_value()) {
407 throw boost::enable_current_exception(InvalidKeyword(
408 name,
409 fmt::format("invalid ESO HIERARCH keyword. No '=' value indicator found: {}", record)));
410 }
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)));
417 }
418 components.value = value;
419 // Optional comment
420 if (comment_start.has_value()) {
421 record.remove_prefix(*comment_start);
422 components.comment = TrimBlanks(record, TrimType::Full);
423 } else {
424 components.comment = std::string_view();
425 }
426
427 return components;
428}
429
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{};
434 components.type = KeywordType::Commentary;
435
436 // Name
437 components.name = TrimBlanks(record.substr(0u, constants::KEYWORD_NAME_LENGTH), TrimType::Name);
438 record.remove_prefix(constants::KEYWORD_NAME_LENGTH);
439 // Value
440 components.value = TrimBlanks(record, TrimType::Full);
441
442 // Commentary keywords does not have a comment
443 components.comment = std::string_view();
444
445 return components;
446}
447
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);
452
453 // Parse name component
454 switch (type) {
456 return ParseValueKeywordComponents(record);
457 case KeywordType::Eso:
458 return ParseEsoKeywordComponents(record);
460 return ParseCommentaryKeywordComponents(record);
461 };
462 std::cerr << __PRETTY_FUNCTION__ << ": Invalid type: " << type << std::endl;
463 std::terminate();
464}
465
466} // namespace
467
469 return lhs.name < rhs.name;
470}
471
473 return lhs.type == rhs.type && lhs.name == rhs.name;
474}
475
477 return !(lhs == rhs);
478}
479
481 std::fill(std::begin(m_record), std::end(m_record), ' ');
482 // cppcheck-suppress throwInNoexceptFunction
483 m_components = ParseKeywordComponents(m_record);
484}
485
486LiteralKeyword::LiteralKeyword(std::array<char, constants::RECORD_LENGTH> record)
487 : m_record(record), m_components(ParseKeywordComponents(m_record)) {
488 try {
489 ValidateKeywordName(GetName());
490 } catch (...) {
491 std::throw_with_nested(std::invalid_argument("Failed to construct LiteralKeyword"));
492 }
493}
494
495LiteralKeyword::LiteralKeyword(std::string_view record) : m_record() {
496 try {
497 if (record.size() > constants::RECORD_LENGTH) {
498 throw boost::enable_current_exception(InvalidKeyword(
499 record, "literalKeyword: Keyword with record > 80 chars is invalid"));
500 }
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());
505 } catch (...) {
506 std::throw_with_nested(std::invalid_argument("Failed to construct LiteralKeyword"));
507 }
508}
509
511 *this = other;
512}
513
514// cppcheck-suppress operatorEqRetRefThis
516 m_record = other.m_record;
517 // Reconstruct string_views using same offset, but with base of this->m_record.data()
518 auto make_offset = [&](std::string_view sv) -> std::string_view {
519 if (sv.empty()) {
520 return std::string_view();
521 }
522 auto start_offset = sv.data() - other.m_record.data();
523 return std::string_view(m_record.data() + start_offset, sv.size());
524 };
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;
529 return *this;
530}
531
532std::string_view LiteralKeyword::GetRecord() const& noexcept {
533 auto full = std::string_view(&m_record[0], m_record.size());
534 return TrimBlanks(full, TrimType::Name);
535}
536
537bool operator<(LiteralKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
538 return lhs.GetName() < rhs.GetName();
539}
540
541bool operator==(LiteralKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
542 return lhs.GetRecord() == rhs.GetRecord();
543}
544
545bool operator!=(LiteralKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
546 return lhs.GetRecord() != rhs.GetRecord();
547}
548
549std::ostream& operator<<(std::ostream& os, LiteralKeyword const& kw) {
550 os << kw.GetRecord();
551 return os;
552}
553
555 LiteralKeyword::Components const& rhs) noexcept -> bool {
556 return lhs.type == rhs.type && lhs.name == rhs.name && lhs.value == rhs.value &&
557 lhs.comment == rhs.comment;
558}
559
561 LiteralKeyword::Components const& rhs) noexcept -> bool {
562 return !operator==(lhs, rhs);
563}
564
565// Instantiate the different variants
567template struct BasicKeyword<EsoKeywordTraits>;
568
569template std::ostream&
571
572template std::ostream&
574
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());
581}
582
583template <class Trait>
585 ValueType value_arg,
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());
589}
590
591template <class Trait>
593 return name == rhs.name && value == rhs.value && comment == rhs.comment;
594}
595
596template <class Trait>
598 return !(*this == rhs);
599}
600
601template <class Trait>
603 return name < rhs.name;
604}
605
606bool NameEquals(KeywordVariant const& lhs, KeywordVariant const& rhs) noexcept {
607 return std::visit(
608 [&](auto const& lhs, auto const& rhs) noexcept -> bool {
609 return lhs.GetName() == rhs.GetName();
610 },
611 lhs,
612 rhs);
613}
614
615KeywordClass GetKeywordClass(std::string_view name) {
616 if (name.size() > FLEN_KEYWORD) {
617 throw boost::enable_current_exception(InvalidKeyword(name, "keyword too long"));
618 }
619 char record[FLEN_CARD] = {' '};
620 std::copy(name.begin(), name.end(), &record[0]);
621
622 return static_cast<KeywordClass>(fits_get_keyclass(record));
623}
624
625bool operator<(LiteralKeyword const& lhs, EsoKeyword const& rhs) noexcept {
626 return lhs.GetName() < rhs.GetName();
627}
628
629bool operator<(LiteralKeyword const& lhs, ValueKeyword const& rhs) noexcept {
630 return lhs.GetName() < rhs.GetName();
631}
632bool operator<(ValueKeyword const& lhs, EsoKeyword const& rhs) noexcept {
633 return lhs.GetName() < rhs.GetName();
634}
635
636bool operator<(ValueKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
637 return lhs.GetName() < rhs.GetName();
638}
639
640bool operator<(EsoKeyword const& lhs, ValueKeyword const& rhs) noexcept {
641 return lhs.GetName() < rhs.GetName();
642}
643bool operator<(EsoKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
644 return lhs.GetName() < rhs.GetName();
645}
646
647std::ostream& operator<<(std::ostream& os, BasicKeywordBase::ValueType const& kw) {
648 std::visit(
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");
655 } else {
656 os << "(" << TypeStr<T>() << ")" << var;
657 }
658 },
659 kw);
660 return os;
661}
662
663template <class Trait>
664std::ostream& operator<<(std::ostream& os, BasicKeyword<Trait> const& kw) {
665 os << "name='" << kw.name << "', value=" << kw.value << ", comment=";
666 if (kw.comment) {
667 os << "'" << *kw.comment << "'";
668 } else {
669 os << "n/a";
670 }
671 return os;
672}
673
674std::ostream& operator<<(std::ostream& os, KeywordVariant const& kw) {
675 std::visit([&](auto const& var) mutable { os << var; }, kw);
676 return os;
677}
678
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); });
683 it != to.end()) {
684 if (policy == ConflictPolicy::Replace) {
685 // Replace existing keyword with same name (and type)
686 *it = kw;
687 }
688 } else {
689 // Otherwise append
690 to.emplace_back(kw);
691 }
692 }
693}
694
696 KeywordVector::iterator position,
697 KeywordVector::const_iterator from_first,
698 KeywordVector::const_iterator from_last) {
699 // Insert in specified position and then delete duplicates from the two ranges before and after
700 // inserted elements.
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) {
704 return NameEquals(val, kw);
705 }) != from_last;
706 };
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);
710 // Invalidates iterators after_inserted_first -> end (note: inserted_first is still valid)
711 keywords.erase(std::remove_if(after_inserted_first, keywords.end(), pred), keywords.end());
712 // Invalidates iterators begin -> inserted_first
713 // cppcheck-suppress invalidContainer
714 keywords.erase(std::remove_if(keywords.begin(), inserted_first, pred), inserted_first);
715}
716
718 using namespace std::string_view_literals;
719 return std::visit(
720 [&](auto& value) -> std::string {
721 using T = std::decay_t<decltype(value)>;
722 if constexpr (std::is_same_v<T, std::string>) {
723 // Escape `'`
724 auto escaped_value = FitsEscapeString(value);
725 if (name.type == daq::fits::KeywordType::Value && name.name == "XTENSION"sv) {
726 // FITS requires XTENSION be padded to at least 8 chars (sec 4.2.1)
727 return fmt::format("'{: <8s}'", escaped_value);
728 } else {
729 return fmt::format("'{}'", escaped_value);
730 }
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>) {
734 if (value > 10000) {
735 return fmt::format("{:g}", value);
736 } else {
737 return fmt::format("{:.1f}", value);
738 }
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);
743 } else {
744 static_assert(always_false_v<T>, "non-exhaustive visitor!");
745 }
746 },
747 value);
748}
749
751 // Fixed value format
752 auto name = keyword.GetName();
753 auto value = FormatFitsValue(name, keyword.value);
754 auto comment = keyword.comment.value_or("");
755 return InternalEsoKeywordFormat(name.name, value, comment);
756}
757
759 // Fixed value format
760 auto name = keyword.GetName();
761 auto value = FormatFitsValue(name, keyword.value);
762 auto comment = keyword.comment.value_or("");
763 return InternalValueKeywordFormat(name.name, value, comment);
764}
765
767 switch (keyword.type) {
768 case KeywordType::Value: {
769 return InternalValueKeywordFormat(keyword.name, keyword.value, keyword.comment);
770 }
771 case KeywordType::Eso: {
772 return InternalEsoKeywordFormat(keyword.name, keyword.value, keyword.comment);
773 }
775 return InternalCommentaryKeywordFormat(keyword.name, keyword.value);
776 }
777 };
778 std::cerr << __PRETTY_FUNCTION__ << ": Invalid type: " << keyword.type << std::endl;
779 std::abort();
780}
781
783 return std::visit(
784 [&](auto& kw) -> LiteralKeyword {
785 using T = std::decay_t<decltype(kw)>;
786 if constexpr (std::is_same_v<T, LiteralKeyword>) {
787 return Format(kw.GetComponents());
788 } else {
789 return Format(kw);
790 }
791 },
792 keyword);
793}
794
795InvalidKeyword::InvalidKeyword(std::string_view name, std::string const& what)
796 : InvalidKeyword(name, what.c_str()) {
797}
798
799InvalidKeyword::InvalidKeyword(std::string_view name, char const* what)
800 : std::invalid_argument(fmt::format("Invalid keyword `{}` {}", name, what)) {
801}
802
803UnknownKeyword::UnknownKeyword(std::string_view name, std::vector<std::string> const& dictionaries)
804 : InvalidKeyword(name,
805 fmt::format("keyword not found in any of the dictionaries: {}",
806 boost::algorithm::join(dictionaries, ", "))) {
807}
808
810}
811
813 return daq::fits::Format(keyword);
814}
815
817UntypedFormat(KeywordNameView name, std::string_view value, std::string_view comment) {
818 switch (name.type) {
820 return InternalValueKeywordFormat(name.name, value, comment);
821 case KeywordType::Eso:
822 return InternalEsoKeywordFormat(name.name, value, comment);
823 default:
824 BOOST_ASSERT_MSG(false, "Invalid keyword type");
825 }
826}
827
828namespace v1 {
829namespace {
830constexpr bool SortEsoKeywordName(std::string_view lhs, std::string_view rhs) noexcept {
831 using namespace std::literals::string_view_literals;
832
833 auto lhs_cat = ParseLogicalCategory(lhs);
834 auto rhs_cat = ParseLogicalCategory(rhs);
835 if (lhs_cat == rhs_cat) {
836 /* within same category keyword is sorted by name */
837 return lhs < rhs;
838 }
839 // This determines the custom category sorting order:
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);
844
845 if (lhs_idx != std::end(categories)) {
846 if (rhs_idx != std::end(categories)) {
847 // Both lhs and rhs are have special case categories. Sort by
848 // their relative position (note: same category was handled before).
849 return std::distance(lhs_idx, rhs_idx) > 0;
850 } else {
851 /* rhs is an unknown category, sort last */
852 return true;
853 }
854 } else {
855 if (rhs_idx != std::end(categories)) {
856 /* Sort rhs before lhs as it has special category*/
857 return false;
858 } else {
859 /* both lhs and rhs are unknown categories -> sort by name */
860 return lhs < rhs;
861 }
862 }
863}
864} // namespace
865
866bool StandardLess::operator()(LiteralKeyword const& lhs_kw, LiteralKeyword const& rhs_kw) noexcept {
867 auto lhs = lhs_kw.GetName();
868 auto rhs = rhs_kw.GetName();
869
870 switch (lhs.type) {
871 case Value:
872 switch (rhs.type) {
873 case Value:
874 /* Value never sorted before other Value (stable sorted) */
875 return false;
876 case Eso:
877 [[fallthrough]];
878 case Commentary:
879 /* lhs Value type sorted before ESO and Commentary types */
880 return true;
881 }
882 case Eso:
883 switch (rhs.type) {
884 case Value:
885 /* lhs is Eso and is sorted after Value */
886 return false;
887 case Eso:
888 /* Both are ESO keywords so sorted by name */
889 return SortEsoKeywordName(lhs.name, rhs.name);
890 case Commentary:
891 /* Eso before commentary */
892 return true;
893 };
894 case Commentary:
895 switch (rhs.type) {
896 case Value:
897 [[fallthrough]];
898 case Eso:
899 /* lhs is Commentary so sorted after both Value and Eso */
900 return false;
901 case Commentary:
902 /* Commentary keywords are stable sorted last */
903 return lhs.name < rhs.name;
904 }
905 };
906 return false;
907}
908
909void StandardSort(std::vector<LiteralKeyword>& keywords) {
910 std::stable_sort(std::begin(keywords), std::end(keywords), StandardLess{});
911}
912
913} // namespace v1
914
915#if defined(UNIT_TEST)
916// Some compile time checks to also verify parsing function are actually constexp.
917static_assert(ParseKeywordType("DATE-OBS= 'date'") == KeywordType::Value);
918static_assert(ParseKeywordType("DATE-OBS='date'") == KeywordType::Commentary);
919static_assert(NextToken(" ='date'", '=', TrimType::Name) ==
920 std::pair<std::string_view, std::optional<std::string_view::size_type>>(" ", 9));
921#endif
922} // namespace daq::fits
Indicates keyword is invalid for some reason.
Definition: keyword.hpp:534
InvalidKeyword(std::string_view name, char const *reason)
Definition: keyword.cpp:799
virtual ~KeywordFormatter() noexcept
Definition: keyword.cpp:809
Represents the literal 80-character FITS keyword record.
Definition: keyword.hpp:129
std::string_view comment
Comment may be empty.
Definition: keyword.hpp:142
LiteralKeyword & operator=(LiteralKeyword const &other) noexcept
Definition: keyword.cpp:515
std::string_view GetRecord() const &noexcept
Definition: keyword.cpp:532
constexpr KeywordNameView GetName() const &noexcept
Query logical keyword name.
Definition: keyword.hpp:190
LiteralKeyword() noexcept
Initializes an empty record (filled with ' ' characters)
Definition: keyword.cpp:480
Decomposed components a literal keyword.
Definition: keyword.hpp:134
auto Format(KeywordVariant const &keyword) const -> LiteralKeyword override
Formats keyword.
Definition: keyword.cpp:812
UnknownKeyword(std::string_view kw, std::vector< std::string > const &dictionaries)
Definition: keyword.cpp:803
Contains data structure for FITS keywords.
void StandardSort(std::vector< LiteralKeyword > &keywords)
Sorts keywords according to ESO DICD standards.
Definition: keyword.cpp:909
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.
Definition: keyword.cpp:695
KeywordType
Type of FITS keyword.
Definition: keyword.hpp:68
@ Eso
An ESO hiearchical keyword.
Definition: keyword.hpp:76
@ Commentary
A commentary keyword, which are keywords that do not fall into the previous categories.
Definition: keyword.hpp:80
@ Value
A value keyword.
Definition: keyword.hpp:72
bool NameEquals(KeywordVariant const &lhs, KeywordVariant const &rhs) noexcept
Compare logical keyword names of keyword of the same type.
Definition: keyword.cpp:606
bool operator==(KeywordNameView lhs, KeywordNameView rhs) noexcept
Definition: keyword.cpp:472
LiteralKeyword Format(KeywordVariant const &keyword)
Definition: keyword.cpp:782
bool operator<(LiteralKeyword const &, LiteralKeyword const &) noexcept
Sort by logical keyword name (not DICD sort)
Definition: keyword.cpp:537
KeywordClass GetKeywordClass(std::string_view name)
Get keyword class.
Definition: keyword.cpp:615
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...
Definition: keyword.cpp:717
auto UntypedFormat(KeywordNameView name, std::string_view value, std::string_view comment) -> LiteralKeyword
Untyped formatting where value already has been formatted correctly.
Definition: keyword.cpp:817
KeywordClass
Fits keyword type.
Definition: keyword.hpp:93
template std::ostream & operator<<< EsoKeywordTraits >(std::ostream &os, BasicKeyword< EsoKeywordTraits > const &kw)
std::vector< KeywordVariant > KeywordVector
Vector of keywords.
Definition: keyword.hpp:423
bool operator!=(KeywordNameView lhs, KeywordNameView rhs) noexcept
Definition: keyword.cpp:476
std::variant< ValueKeyword, EsoKeyword, LiteralKeyword > KeywordVariant
The different variants of keywords that are supported.
Definition: keyword.hpp:409
void UpdateKeywords(KeywordVector &to, KeywordVector const &from, ConflictPolicy policy=ConflictPolicy::Replace)
Updates to with keywords from from.
Definition: keyword.cpp:679
std::ostream & operator<<(std::ostream &os, HduType hdu_type)
Format HduType hdu_type to os.
Definition: cfitsio.cpp:56
@ Replace
Replace keyword that conflicts.
std::variant< std::string, std::int64_t, std::uint64_t, double, bool > ValueType
Definition: keyword.hpp:252
A type safe version of LiteralKeyword that consist of the three basic components of a FITS keyword ke...
Definition: keyword.hpp:275
bool operator==(BasicKeyword const &rhs) const noexcept
Compares all members for equality.
Definition: keyword.cpp:592
bool operator!=(BasicKeyword const &rhs) const noexcept
Compares all members for inequality.
Definition: keyword.cpp:597
bool operator<(BasicKeyword const &rhs) const noexcept
Uses name property as the sorting index.
Definition: keyword.cpp:602
std::string name
Trimmed keyword name.
Definition: keyword.hpp:315
std::optional< std::string > comment
Trimmed keyword comment.
Definition: keyword.hpp:320
constexpr KeywordNameView GetName() const &noexcept
Query logical keyword name.
Definition: keyword.hpp:294
std::string_view name
Definition: keyword.hpp:84
Sorting function object.
Definition: keyword.hpp:604