ifw-daq  3.0.0-pre2
IFW Data Acquisition modules
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  */
8 #include <daq/fits/keyword.hpp>
9 
10 #include <algorithm>
11 #include <cstdlib>
12 #include <iostream>
13 #include <ostream>
14 
15 #include <fmt/format.h>
16 
17 namespace {
18 template <class>
19 inline constexpr bool always_false_v = false; // NOLINT
20 template <class T>
21 char const* TypeStr() noexcept;
22 template <>
23 char const* TypeStr<uint64_t>() noexcept {
24  return "uint64_t";
25 }
26 template <>
27 char const* TypeStr<int64_t>() noexcept {
28  return "int64_t";
29 }
30 template <>
31 char const* TypeStr<double>() noexcept {
32  return "double";
33 }
34 
35 std::string FormatFitsValue(daq::fits::KeywordNameView name,
37  using namespace std::string_view_literals;
38  return std::visit(
39  [&](auto& value) -> std::string {
40  using T = std::decay_t<decltype(value)>;
41  if constexpr (std::is_same_v<T, std::string>) {
42  if (name.type == daq::fits::KeywordType::Value && name.name == "XTENSION"sv) {
43  // FITS requires XTENSION be padded to at least 8 chars (sec 4.2.1)
44  return fmt::format("'{: <8s}'", value);
45  } else {
46  return fmt::format("'{}'", value);
47  }
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>) {
51  if (value > 10000) {
52  return fmt::format("{:g}", value);
53  } else {
54  return fmt::format("{:.1f}", value);
55  }
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);
60  } else {
61  static_assert(always_false_v<T>, "non-exhaustive visitor!");
62  }
63  },
64  value);
65 }
66 
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} {}";
72 
73 } // namespace
74 
75 namespace daq::fits {
76 namespace {
77 LiteralKeyword InternalValueKeywordFormat(std::string_view name,
78  std::string_view value,
79  std::string_view comment,
80  bool is_string) {
81  // Fixed value format
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));
89  }
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);
93 }
94 
95 LiteralKeyword
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));
103  }
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);
107 }
108 
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));
116  }
117  return LiteralKeyword(formatted_name_value);
118 }
119 
120 /**
121  * Validates logical keyword name according to FITS rules.
122  * The rules depend on the type of keyword.
123  *
124  * Common rules:
125  *
126  * - Trailing whitespace are insignifant.
127  * - Valid character set: [A-Z0-9_-]
128  *
129  * KeywordType::Value and KeywordType::Commentary:
130  * - Size: 1-8 (for an all-spaces keyword there must be one ' ')
131  *
132  * KeywordType::Eso:
133  *
134  * - Size: 1 - ESO_HIERARCH_MAX_NAME_LENGTH
135  *
136  * @throws std::invalid_argument on errors
137  */
138 void ValidateKeywordName(KeywordNameView keyword) {
139  if (keyword.name.empty()) {
140  throw std::invalid_argument("Keyword name cannot be empty");
141  }
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 {}",
146  max_length,
147  keyword.name,
148  keyword.name.size());
149  throw std::invalid_argument(msg);
150  }
151  auto is_valid_char = [](char c) -> bool {
152  return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || c == ' ' || c == '_' || c == '-';
153  };
154 
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_-].",
158  keyword.name));
159  }
160 }
161 
162 constexpr KeywordType ParseKeywordType(std::string_view record) noexcept {
163  if (record.size() >= constants::KEYWORD_NAME_LENGTH + constants::VALUE_INDICATOR.size()) {
164  // If bytes 9 and 10 contain the value indicator it must be a value keyword
165  if (record.substr(constants::KEYWORD_NAME_LENGTH, constants::VALUE_INDICATOR.size()) ==
166  constants::VALUE_INDICATOR) {
167  return KeywordType::Value;
168  }
169  if (record.size() > constants::ESO_HIERARCH_PREFIX.size()) {
170  // HIERARCH ESO keyword must start with 'HIERARCH ESO ' and contain '='.
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) {
174  return KeywordType::Eso;
175  }
176  }
177  }
178  // Everything else are Commentary keywords.
180 }
181 
182 /**
183  * Parse the logical category of an ESO hierarchical keyword.
184  *
185  * Logical category ignores trailing numbers, so logical category of
186  * - `DET1` is `DET`
187  * - `DET01` is `DET`
188  *
189  * However, the logical category for
190  * - `DET1A` is `DET1A`
191  * - `DET1A1` is `DET1A`.
192  *
193  * @param name Logical keyword name with the first token being the category.
194  * @return logical category (first token stripped of trailing numbers)
195  */
196 constexpr std::string_view ParseLogicalCategory(std::string_view name) noexcept {
197  using namespace std::literals::string_view_literals;
198  // End of category token
199  auto end = name.find_first_of(" ="sv);
200  // Trim away any trailing numbers
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);
205  return view;
206  }
207  return full_category;
208 }
209 
210 enum class TrimType { Name, Full };
211 
212 /**
213  * Trim string view.
214  * If @a trim is @a Name it will try to keep a single space char because that is significant in
215  * FITS.
216  */
217 constexpr std::string_view TrimBlanks(std::string_view str, TrimType trim) noexcept {
218  if (str.empty()) {
219  return str;
220  }
221  { // Trim left
222  auto trim_pos = str.find_first_not_of(constants::BLANK_CHARS);
223  if (trim_pos == std::string_view::npos) {
224  // All blanks
225  return trim == TrimType::Name ? str.substr(0u, 1u) : std::string_view();
226  }
227  str.remove_prefix(trim_pos);
228  }
229  { // Trim right
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);
233  }
234  }
235  return str;
236 }
237 
238 /**
239  * Read next token in str up to @a delimiter.
240  *
241  * @returns Trimmed token and position after delimiter in the input str. If delimiter was not found
242  * the returned string_view iterator will be nullopt.
243  */
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) {
249  // Delimiter found
250  ret_pos = delimiter_pos + 1;
251  str.remove_suffix(str.size() - delimiter_pos);
252  }
253  auto trimmed = TrimBlanks(str, trim);
254  return {trimmed, ret_pos};
255 }
256 
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{};
261  components.type = KeywordType::Value;
262 
263  // Name
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());
266  // Value
267  auto [value, comment_start] = NextToken(record, '/', TrimType::Full);
268  components.value = value;
269  // Optional comment
270  if (comment_start.has_value()) {
271  record.remove_prefix(*comment_start);
272  components.comment = TrimBlanks(record, TrimType::Full);
273  } else {
274  components.comment = std::string_view();
275  }
276 
277  return components;
278 }
279 
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{};
284  components.type = KeywordType::Eso;
285  // Name
286  record.remove_prefix(constants::ESO_HIERARCH_PREFIX.size());
287 
288  auto [name, value_start] = NextToken(record, '=', TrimType::Name);
289  components.name = name;
290  // Value
291  if (!value_start.has_value()) {
292  throw std::invalid_argument(
293  fmt::format("Invalid ESO HIERARCH keyword. No '=' value indicator found: {}", record));
294  }
295  record.remove_prefix(*value_start);
296  auto [value, comment_start] = NextToken(record, '/', TrimType::Full);
297  if (value.empty()) {
298  throw std::invalid_argument(
299  fmt::format("Invalid ESO HIERARCH keyword. No value found: {}", record));
300  }
301  components.value = value;
302  // Optional comment
303  if (comment_start.has_value()) {
304  record.remove_prefix(*comment_start);
305  components.comment = TrimBlanks(record, TrimType::Full);
306  } else {
307  components.comment = std::string_view();
308  }
309 
310  return components;
311 }
312 
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{};
317  components.type = KeywordType::Commentary;
318 
319  // Name
320  components.name = TrimBlanks(record.substr(0u, constants::KEYWORD_NAME_LENGTH), TrimType::Name);
321  record.remove_prefix(constants::KEYWORD_NAME_LENGTH);
322  // Value
323  components.value = TrimBlanks(record, TrimType::Full);
324 
325  // Commentary keywords does not have a comment
326  components.comment = std::string_view();
327 
328  return components;
329 }
330 
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);
335 
336  // Parse name component
337  switch (type) {
338  case KeywordType::Value:
339  return ParseValueKeywordComponents(record);
340  case KeywordType::Eso:
341  return ParseEsoKeywordComponents(record);
343  return ParseCommentaryKeywordComponents(record);
344  };
345  std::cerr << __PRETTY_FUNCTION__ << ": Invalid type: " << type << std::endl;
346  std::terminate();
347 }
348 
349 } // namespace
350 
351 bool operator<(KeywordNameView lhs, KeywordNameView rhs) noexcept {
352  return lhs.name < rhs.name;
353 }
354 
355 bool operator==(KeywordNameView lhs, KeywordNameView rhs) noexcept {
356  return lhs.type == rhs.type && lhs.name == rhs.name;
357 }
358 
359 bool operator!=(KeywordNameView lhs, KeywordNameView rhs) noexcept {
360  return !(lhs == rhs);
361 }
362 
364  std::fill(std::begin(m_record), std::end(m_record), ' ');
365  // cppcheck-suppress throwInNoexceptFunction
366  m_components = ParseKeywordComponents(m_record);
367 }
368 
369 LiteralKeyword::LiteralKeyword(std::array<char, constants::RECORD_LENGTH> record)
370  : m_record(record), m_components(ParseKeywordComponents(m_record)) {
371  try {
372  ValidateKeywordName(GetName());
373  } catch (...) {
374  std::throw_with_nested(std::invalid_argument("Failed to construct LiteralKeyword"));
375  }
376 }
377 
378 LiteralKeyword::LiteralKeyword(std::string_view record) : m_record() {
379  try {
380  if (record.size() > constants::RECORD_LENGTH) {
381  throw std::invalid_argument(
382  "LiteralKeyword: Keyword with record > 80 chars is invalid");
383  }
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());
388  } catch (...) {
389  std::throw_with_nested(std::invalid_argument("Failed to construct LiteralKeyword"));
390  }
391 }
392 
394  *this = other;
395 }
396 
397 // cppcheck-suppress operatorEqRetRefThis
399  m_record = other.m_record;
400  // Reconstruct string_views using same offset, but with base of this->m_record.data()
401  auto make_offset = [&](std::string_view sv) -> std::string_view {
402  if (sv.empty()) {
403  return std::string_view();
404  }
405  auto start_offset = sv.data() - other.m_record.data();
406  return std::string_view(m_record.data() + start_offset, sv.size());
407  };
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;
412  return *this;
413 }
414 
415 std::string_view LiteralKeyword::GetRecord() const& noexcept {
416  auto full = std::string_view(&m_record[0], m_record.size());
417  return TrimBlanks(full, TrimType::Name);
418 }
419 
420 bool operator<(LiteralKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
421  return lhs.GetName() < rhs.GetName();
422 }
423 
424 bool operator==(LiteralKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
425  return lhs.GetRecord() == rhs.GetRecord();
426 }
427 
428 bool operator!=(LiteralKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
429  return lhs.GetRecord() != rhs.GetRecord();
430 }
431 
432 std::ostream& operator<<(std::ostream& os, LiteralKeyword const& kw) {
433  os << kw.GetRecord();
434  return os;
435 }
436 
437 // Instantiate the different variants
438 template struct BasicKeyword<ValueKeywordTraits>;
439 template struct BasicKeyword<EsoKeywordTraits>;
440 
441 template std::ostream&
443 
444 template std::ostream&
446 
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());
453 }
454 
455 template <class Trait>
457  ValueType value_arg,
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());
461 }
462 
463 template <class Trait>
464 bool BasicKeyword<Trait>::operator==(BasicKeyword<Trait> const& rhs) const noexcept {
465  return name == rhs.name && value == rhs.value && comment == rhs.comment;
466 }
467 
468 template <class Trait>
469 bool BasicKeyword<Trait>::operator!=(BasicKeyword<Trait> const& rhs) const noexcept {
470  return !(*this == rhs);
471 }
472 
473 template <class Trait>
474 bool BasicKeyword<Trait>::operator<(BasicKeyword<Trait> const& rhs) const noexcept {
475  return name < rhs.name;
476 }
477 
478 bool NameEquals(KeywordVariant const& lhs, KeywordVariant const& rhs) noexcept {
479  if (lhs.index() != rhs.index()) {
480  return false;
481  }
482  return std::visit(
483  [&](auto const& lhs, auto const& rhs) noexcept -> bool {
484  return lhs.GetName() == rhs.GetName();
485  },
486  lhs,
487  rhs);
488 }
489 
490 KeywordClass GetKeywordClass(std::string_view name) {
491  if (name.size() > FLEN_KEYWORD) {
492  throw std::invalid_argument("Keyword too long");
493  }
494  char record[FLEN_CARD] = {' '};
495  std::copy(name.begin(), name.end(), &record[0]);
496 
497  return static_cast<KeywordClass>(fits_get_keyclass(record));
498 }
499 
500 bool operator<(LiteralKeyword const& lhs, EsoKeyword const& rhs) noexcept {
501  return lhs.GetName() < rhs.GetName();
502 }
503 
504 bool operator<(LiteralKeyword const& lhs, ValueKeyword const& rhs) noexcept {
505  return lhs.GetName() < rhs.GetName();
506 }
507 bool operator<(ValueKeyword const& lhs, EsoKeyword const& rhs) noexcept {
508  return lhs.GetName() < rhs.GetName();
509 }
510 
511 bool operator<(ValueKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
512  return lhs.GetName() < rhs.GetName();
513 }
514 
515 bool operator<(EsoKeyword const& lhs, ValueKeyword const& rhs) noexcept {
516  return lhs.GetName() < rhs.GetName();
517 }
518 bool operator<(EsoKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
519  return lhs.GetName() < rhs.GetName();
520 }
521 
522 std::ostream& operator<<(std::ostream& os, BasicKeywordBase::ValueType const& kw) {
523  std::visit(
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");
530  } else {
531  os << "(" << TypeStr<T>() << ")" << var;
532  }
533  },
534  kw);
535  return os;
536 }
537 
538 template <class Trait>
539 std::ostream& operator<<(std::ostream& os, BasicKeyword<Trait> const& kw) {
540  os << "name='" << kw.name << "', value=" << kw.value << ", comment=";
541  if (kw.comment) {
542  os << "'" << *kw.comment << "'";
543  } else {
544  os << "n/a";
545  }
546  return os;
547 }
548 
549 std::ostream& operator<<(std::ostream& os, KeywordVariant const& kw) {
550  std::visit([&](auto const& var) mutable { os << var; }, kw);
551  return os;
552 }
553 
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); });
558  it != to.end()) {
559  if (policy == ConflictPolicy::Replace) {
560  // Replace existing keyword with same name (and type)
561  *it = kw;
562  }
563  } else {
564  // Otherwise append
565  to.emplace_back(kw);
566  }
567  }
568 }
569 
571  KeywordVector::iterator position,
572  KeywordVector::const_iterator from_first,
573  KeywordVector::const_iterator from_last) {
574  // Insert in specified position and then delete duplicates from the two ranges before and after
575  // inserted elements.
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) {
579  return NameEquals(val, kw);
580  }) != from_last;
581  };
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);
585  // Invalidates iterators after_inserted_first -> end (note: inserted_first is still valid)
586  keywords.erase(std::remove_if(after_inserted_first, keywords.end(), pred), keywords.end());
587  // Invalidates iterators begin -> inserted_first
588  // cppcheck-suppress invalidContainer
589  keywords.erase(std::remove_if(keywords.begin(), inserted_first, pred), inserted_first);
590 }
591 
593  // Fixed value format
594  auto name = keyword.GetName();
595  auto value = FormatFitsValue(name, keyword.value);
596  auto comment = keyword.comment.value_or("");
597  return InternalEsoKeywordFormat(name.name, value, comment);
598 }
599 
601  // Fixed value format
602  auto name = keyword.GetName();
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));
607 }
608 
610  switch (keyword.type) {
611  case KeywordType::Value: {
612  bool is_str = keyword.value.find('\'') != std::string_view::npos;
613  return InternalValueKeywordFormat(keyword.name, keyword.value, keyword.comment, is_str);
614  }
615  case KeywordType::Eso: {
616  return InternalEsoKeywordFormat(keyword.name, keyword.value, keyword.comment);
617  }
619  return InternalCommentaryKeywordFormat(keyword.name, keyword.value);
620  }
621  };
622  std::cerr << __PRETTY_FUNCTION__ << ": Invalid type: " << keyword.type << std::endl;
623  std::abort();
624 }
625 
627  return std::visit(
628  [&](auto& kw) -> LiteralKeyword {
629  using T = std::decay_t<decltype(kw)>;
630  if constexpr (std::is_same_v<T, LiteralKeyword>) {
631  return Format(kw.GetComponents());
632  } else {
633  return Format(kw);
634  }
635  },
636  keyword);
637 }
638 
639 namespace v1 {
640 namespace {
641 constexpr bool SortEsoKeywordName(std::string_view lhs, std::string_view rhs) noexcept {
642  using namespace std::literals::string_view_literals;
643 
644  auto lhs_cat = ParseLogicalCategory(lhs);
645  auto rhs_cat = ParseLogicalCategory(rhs);
646  if (lhs_cat == rhs_cat) {
647  /* within same category keyword is sorted by name */
648  return lhs < rhs;
649  }
650  // This determines the custom category sorting order:
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);
655 
656  if (lhs_idx != std::end(categories)) {
657  if (rhs_idx != std::end(categories)) {
658  // Both lhs and rhs are have special case categories. Sort by
659  // their relative position (note: same category was handled before).
660  return std::distance(lhs_idx, rhs_idx) > 0;
661  } else {
662  /* rhs is an unknown category, sort last */
663  return true;
664  }
665  } else {
666  if (rhs_idx != std::end(categories)) {
667  /* Sort rhs before lhs as it has special category*/
668  return false;
669  } else {
670  /* both lhs and rhs are unknown categories -> sort by name */
671  return lhs < rhs;
672  }
673  }
674 }
675 } // namespace
676 
677 bool StandardLess::operator()(LiteralKeyword const& lhs_kw, LiteralKeyword const& rhs_kw) noexcept {
678  auto lhs = lhs_kw.GetName();
679  auto rhs = rhs_kw.GetName();
680 
681  switch (lhs.type) {
682  case Value:
683  switch (rhs.type) {
684  case Value:
685  /* Value never sorted before other Value (stable sorted) */
686  return false;
687  case Eso:
688  [[fallthrough]];
689  case Commentary:
690  /* lhs Value type sorted before ESO and Commentary types */
691  return true;
692  }
693  case Eso:
694  switch (rhs.type) {
695  case Value:
696  /* lhs is Eso and is sorted after Value */
697  return false;
698  case Eso:
699  /* Both are ESO keywords so sorted by name */
700  return SortEsoKeywordName(lhs.name, rhs.name);
701  case Commentary:
702  /* Eso before commentary */
703  return true;
704  };
705  case Commentary:
706  switch (rhs.type) {
707  case Value:
708  [[fallthrough]];
709  case Eso:
710  /* lhs is Commentary so sorted after both Value and Eso */
711  return false;
712  case Commentary:
713  /* Commentary keywords are stable sorted last */
714  return lhs.name < rhs.name;
715  }
716  };
717  return false;
718 }
719 
720 void StandardSort(std::vector<LiteralKeyword>& keywords) {
721  std::stable_sort(std::begin(keywords), std::end(keywords), StandardLess{});
722 }
723 
724 } // namespace v1
725 
726 #if defined(UNIT_TEST)
727 // Some compile time checks to also verify parsing function are actually constexp.
728 static_assert(ParseKeywordType("DATE-OBS= 'date'") == KeywordType::Value);
729 static_assert(ParseKeywordType("DATE-OBS='date'") == KeywordType::Commentary);
730 static_assert(NextToken(" ='date'", '=', TrimType::Name) ==
731  std::pair<std::string_view, std::optional<std::string_view::size_type>>(" ", 9));
732 #endif
733 } // namespace daq::fits
Represents the literal 80-character FITS keyword record.
Definition: keyword.hpp:125
std::string_view comment
Comment may be empty.
Definition: keyword.hpp:138
LiteralKeyword & operator=(LiteralKeyword const &other) noexcept
Definition: keyword.cpp:398
std::string_view GetRecord() const &noexcept
Definition: keyword.cpp:415
constexpr KeywordNameView GetName() const &noexcept
Query logical keyword name.
Definition: keyword.hpp:186
LiteralKeyword() noexcept
Initializes an empty record (filled with ' ' characters)
Definition: keyword.cpp:363
Decomposed components a literal keyword.
Definition: keyword.hpp:130
Contains data structure for FITS keywords.
void StandardSort(std::vector< LiteralKeyword > &keywords)
Sorts keywords according to ESO DICD standards.
Definition: keyword.cpp:720
void InsertKeywords(KeywordVector &keywords, KeywordVector::iterator position, KeywordVector::const_iterator from_first, KeywordVector::const_iterator from_last)
Insert keywords.
Definition: keyword.cpp:570
KeywordType
Type of FITS keyword.
Definition: keyword.hpp:64
@ Eso
An ESO hiearchical keyword.
Definition: keyword.hpp:72
@ Commentary
A commentary keyword, which are keywords that do not fall into the previous categories.
Definition: keyword.hpp:76
@ Value
A value keyword.
Definition: keyword.hpp:68
bool NameEquals(KeywordVariant const &lhs, KeywordVariant const &rhs) noexcept
Compare logical keyword names of keyword of the same type.
Definition: keyword.cpp:478
bool operator==(KeywordNameView lhs, KeywordNameView rhs) noexcept
Definition: keyword.cpp:355
LiteralKeyword Format(KeywordVariant const &keyword)
Definition: keyword.cpp:626
bool operator<(LiteralKeyword const &, LiteralKeyword const &) noexcept
Sort by logical keyword name (not DICD sort)
Definition: keyword.cpp:420
template std::ostream & operator<<< ValueKeywordTraits >(std::ostream &os, BasicKeyword< ValueKeywordTraits > const &kw)
KeywordClass GetKeywordClass(std::string_view name)
Get keyword class.
Definition: keyword.cpp:490
KeywordClass
Fits keyword type.
Definition: keyword.hpp:89
std::vector< KeywordVariant > KeywordVector
Vector of keywords.
Definition: keyword.hpp:414
bool operator!=(KeywordNameView lhs, KeywordNameView rhs) noexcept
Definition: keyword.cpp:359
std::variant< ValueKeyword, EsoKeyword, LiteralKeyword > KeywordVariant
The different variants of keywords that are supported.
Definition: keyword.hpp:400
void UpdateKeywords(KeywordVector &to, KeywordVector const &from, ConflictPolicy policy=ConflictPolicy::Replace)
Updates to with keywords from from.
Definition: keyword.cpp:554
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)
Definition: keyword.cpp:432
std::variant< std::string, int64_t, uint64_t, double, bool > ValueType
Definition: keyword.hpp:243
A type safe version of LiteralKeyword that consist of the three basic components of a FITS keyword ke...
Definition: keyword.hpp:266
bool operator==(BasicKeyword const &rhs) const noexcept
Compares all members for equality.
Definition: keyword.cpp:464
bool operator!=(BasicKeyword const &rhs) const noexcept
Compares all members for inequality.
Definition: keyword.cpp:469
bool operator<(BasicKeyword const &rhs) const noexcept
Uses name property as the sorting index.
Definition: keyword.cpp:474
std::string name
Trimmed keyword name.
Definition: keyword.hpp:306
std::optional< std::string > comment
Trimmed keyword comment.
Definition: keyword.hpp:311
constexpr KeywordNameView GetName() const &noexcept
Query logical keyword name.
Definition: keyword.hpp:285
std::string_view name
Definition: keyword.hpp:80
Sorting function object.
Definition: keyword.hpp:533