ifw-daq  2.1.0-pre1
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 <cassert>
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) {
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]] 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  assert(false);
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  m_components = ParseKeywordComponents(m_record);
366  ValidateKeywordName(GetName());
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 
398  m_record = other.m_record;
399  // Reconstruct string_views using same offset, but with base of this->m_record.data()
400  auto make_offset = [&](std::string_view sv) -> std::string_view {
401  if (sv.empty()) {
402  return std::string_view();
403  }
404  auto start_offset = sv.data() - other.m_record.data();
405  return std::string_view(m_record.data() + start_offset, sv.size());
406  };
407  m_components.name = make_offset(other.m_components.name);
408  m_components.value = make_offset(other.m_components.value);
409  m_components.comment = make_offset(other.m_components.comment);
410  m_components.type = other.m_components.type;
411  return *this;
412 }
413 
414 std::string_view LiteralKeyword::GetRecord() const& noexcept {
415  auto full = std::string_view(&m_record[0], m_record.size());
416  return TrimBlanks(full, TrimType::Name);
417 }
418 
419 bool operator<(LiteralKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
420  return lhs.GetName() < rhs.GetName();
421 }
422 
423 bool operator==(LiteralKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
424  return lhs.GetRecord() == rhs.GetRecord();
425 }
426 
427 bool operator!=(LiteralKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
428  return lhs.GetRecord() != rhs.GetRecord();
429 }
430 
431 std::ostream& operator<<(std::ostream& os, LiteralKeyword const& kw) {
432  os << kw.GetRecord();
433  return os;
434 }
435 
436 // Instantiate the different variants
437 template struct BasicKeyword<ValueKeywordTraits>;
438 template struct BasicKeyword<EsoKeywordTraits>;
439 
440 template std::ostream&
442 
443 template std::ostream&
445 
446 template <class Trait>
448  char const* string_value,
449  std::optional<std::string> comment_arg)
450  : name(std::move(name_arg)), value(std::string(string_value)), comment(std::move(comment_arg)) {
451  ValidateKeywordName(GetName());
452 }
453 
454 template <class Trait>
456  ValueType value_arg,
457  std::optional<std::string> comment_arg)
458  : name(std::move(name_arg)), value(std::move(value_arg)), comment(std::move(comment_arg)) {
459  ValidateKeywordName(GetName());
460 }
461 
462 template <class Trait>
463 bool BasicKeyword<Trait>::operator==(BasicKeyword<Trait> const& rhs) const noexcept {
464  return name == rhs.name && value == rhs.value && comment == rhs.comment;
465 }
466 
467 template <class Trait>
468 bool BasicKeyword<Trait>::operator!=(BasicKeyword<Trait> const& rhs) const noexcept {
469  return !(*this == rhs);
470 }
471 
472 template <class Trait>
473 bool BasicKeyword<Trait>::operator<(BasicKeyword<Trait> const& rhs) const noexcept {
474  return name < rhs.name;
475 }
476 
477 bool NameEquals(KeywordVariant const& lhs, KeywordVariant const& rhs) noexcept {
478  if (lhs.index() != rhs.index()) {
479  return false;
480  }
481  return std::visit(
482  [&](auto const& lhs, auto const& rhs) noexcept -> bool {
483  return lhs.GetName() == rhs.GetName();
484  },
485  lhs,
486  rhs);
487 }
488 
489 KeywordClass GetKeywordClass(std::string_view name) {
490  if (name.size() > FLEN_KEYWORD) {
491  throw std::invalid_argument("Keyword too long");
492  }
493  char record[FLEN_CARD] = {' '};
494  std::copy(name.begin(), name.end(), &record[0]);
495 
496  return static_cast<KeywordClass>(fits_get_keyclass(record));
497 }
498 
499 bool operator<(LiteralKeyword const& lhs, EsoKeyword const& rhs) noexcept {
500  return lhs.GetName() < rhs.GetName();
501 }
502 
503 bool operator<(LiteralKeyword const& lhs, ValueKeyword const& rhs) noexcept {
504  return lhs.GetName() < rhs.GetName();
505 }
506 bool operator<(ValueKeyword const& lhs, EsoKeyword const& rhs) noexcept {
507  return lhs.GetName() < rhs.GetName();
508 }
509 
510 bool operator<(ValueKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
511  return lhs.GetName() < rhs.GetName();
512 }
513 
514 bool operator<(EsoKeyword const& lhs, ValueKeyword const& rhs) noexcept {
515  return lhs.GetName() < rhs.GetName();
516 }
517 bool operator<(EsoKeyword const& lhs, LiteralKeyword const& rhs) noexcept {
518  return lhs.GetName() < rhs.GetName();
519 }
520 
521 std::ostream& operator<<(std::ostream& os, BasicKeywordBase::ValueType const& kw) {
522  std::visit(
523  [&](auto const& var) mutable {
524  using T = std::decay_t<decltype(var)>;
525  if constexpr (std::is_same_v<T, std::string>) {
526  os << "(str)'" << var << "'";
527  } else if constexpr (std::is_same_v<T, bool>) {
528  os << "(bool)" << (var ? "true" : "false");
529  } else {
530  os << "(" << TypeStr<T>() << ")" << var;
531  }
532  },
533  kw);
534  return os;
535 }
536 
537 template <class Trait>
538 std::ostream& operator<<(std::ostream& os, BasicKeyword<Trait> const& kw) {
539  os << "name='" << kw.name << "', value=" << kw.value << ", comment=";
540  if (kw.comment) {
541  os << "'" << *kw.comment << "'";
542  } else {
543  os << "n/a";
544  }
545  return os;
546 }
547 
548 std::ostream& operator<<(std::ostream& os, KeywordVariant const& kw) {
549  std::visit([&](auto const& var) mutable { os << var; }, kw);
550  return os;
551 }
552 
554  for (auto const& kw : from) {
555  if (auto it = std::find_if(
556  to.begin(), to.end(), [&](auto const& val) { return NameEquals(val, kw); });
557  it != to.end()) {
558  if (policy == ConflictPolicy::Replace) {
559  // Replace existing keyword with same name (and type)
560  *it = kw;
561  }
562  } else {
563  // Otherwise append
564  to.emplace_back(kw);
565  }
566  }
567 }
568 
570  KeywordVector::iterator position,
571  KeywordVector::const_iterator from_first,
572  KeywordVector::const_iterator from_last) {
573  // Insert in specified position and then delete duplicates from the two ranges before and after
574  // inserted elements.
575  auto num_elements = std::distance(from_first, from_last);
576  auto pred = [&](KeywordVector::value_type const& kw) -> bool {
577  return std::find_if(from_first, from_last, [&](auto const& val) {
578  return NameEquals(val, kw);
579  }) != from_last;
580  };
581  auto inserted_first = keywords.insert(position, from_first, from_last);
582  auto after_inserted_first = inserted_first;
583  std::advance(after_inserted_first, num_elements);
584  keywords.erase(std::remove_if(after_inserted_first, keywords.end(), pred), keywords.end());
585  keywords.erase(std::remove_if(keywords.begin(), inserted_first, pred), inserted_first);
586 }
587 
589  // Fixed value format
590  auto name = keyword.GetName();
591  auto value = FormatFitsValue(name, keyword.value);
592  auto comment = keyword.comment.value_or("");
593  return InternalEsoKeywordFormat(name.name, value, comment);
594 }
595 
597  // Fixed value format
598  auto name = keyword.GetName();
599  auto value = FormatFitsValue(name, keyword.value);
600  auto comment = keyword.comment.value_or("");
601  return InternalValueKeywordFormat(
602  name.name, value, comment, std::holds_alternative<std::string>(keyword.value));
603 }
604 
606  switch (keyword.type) {
607  case KeywordType::Value: {
608  bool is_str = keyword.value.find('\'') != std::string_view::npos;
609  return InternalValueKeywordFormat(keyword.name, keyword.value, keyword.comment, is_str);
610  }
611  case KeywordType::Eso: {
612  return InternalEsoKeywordFormat(keyword.name, keyword.value, keyword.comment);
613  }
615  return InternalCommentaryKeywordFormat(keyword.name, keyword.value);
616  }
617  };
618  std::cerr << __PRETTY_FUNCTION__ << ": Invalid type: " << keyword.type << std::endl;
619  assert(false);
620 }
621 
623  return std::visit(
624  [&](auto& kw) -> LiteralKeyword {
625  using T = std::decay_t<decltype(kw)>;
626  if constexpr (std::is_same_v<T, LiteralKeyword>) {
627  return Format(kw.GetComponents());
628  } else {
629  return Format(kw);
630  }
631  },
632  keyword);
633 }
634 
635 namespace v1 {
636 namespace {
637 constexpr bool SortEsoKeywordName(std::string_view lhs, std::string_view rhs) noexcept {
638  using namespace std::literals::string_view_literals;
639 
640  auto lhs_cat = ParseLogicalCategory(lhs);
641  auto rhs_cat = ParseLogicalCategory(rhs);
642  if (lhs_cat == rhs_cat) {
643  /* within same category keyword is sorted by name */
644  return lhs < rhs;
645  }
646  // This determines the custom category sorting order:
647  std::array<std::string_view, 8> categories = {
648  "DPR"sv, "OBS"sv, "TPL"sv, "GEN"sv, "TEL"sv, "ADA"sv, "INS"sv, "DET"sv};
649  auto lhs_idx = std::find(std::begin(categories), std::end(categories), lhs_cat);
650  auto rhs_idx = std::find(std::begin(categories), std::end(categories), rhs_cat);
651 
652  if (lhs_idx != std::end(categories)) {
653  if (rhs_idx != std::end(categories)) {
654  // Both lhs and rhs are have special case categories. Sort by
655  // their relative position (note: same category was handled before).
656  return std::distance(lhs_idx, rhs_idx) > 0;
657  } else {
658  /* rhs is an unknown category, sort last */
659  return true;
660  }
661  } else {
662  if (rhs_idx != std::end(categories)) {
663  /* Sort rhs before lhs as it has special category*/
664  return false;
665  } else {
666  /* both lhs and rhs are unknown categories -> sort by name */
667  return lhs < rhs;
668  }
669  }
670 }
671 } // namespace
672 
673 bool StandardLess::operator()(LiteralKeyword const& lhs_kw, LiteralKeyword const& rhs_kw) noexcept {
674  auto lhs = lhs_kw.GetName();
675  auto rhs = rhs_kw.GetName();
676 
677  switch (lhs.type) {
678  case Value:
679  switch (rhs.type) {
680  case Value:
681  /* Value never sorted before other Value (stable sorted) */
682  return false;
683  case Eso:
684  [[fallthrough]];
685  case Commentary:
686  /* lhs Value type sorted before ESO and Commentary types */
687  return true;
688  }
689  case Eso:
690  switch (rhs.type) {
691  case Value:
692  /* lhs is Eso and is sorted after Value */
693  return false;
694  case Eso:
695  /* Both are ESO keywords so sorted by name */
696  return SortEsoKeywordName(lhs.name, rhs.name);
697  case Commentary:
698  /* Eso before commentary */
699  return true;
700  };
701  case Commentary:
702  switch (rhs.type) {
703  case Value:
704  [[fallthrough]];
705  case Eso:
706  /* lhs is Commentary so sorted after both Value and Eso */
707  return false;
708  case Commentary:
709  /* Commentary keywords are stable sorted last */
710  return lhs.name < rhs.name;
711  }
712  };
713  return false;
714 }
715 
716 void StandardSort(std::vector<LiteralKeyword>& keywords) {
717  std::stable_sort(std::begin(keywords), std::end(keywords), StandardLess{});
718 }
719 
720 } // namespace v1
721 
722 #if defined(UNIT_TEST)
723 // Some compile time checks to also verify parsing function are actually constexp.
724 static_assert(ParseKeywordType("DATE-OBS= 'date'") == KeywordType::Value);
725 static_assert(ParseKeywordType("DATE-OBS='date'") == KeywordType::Commentary);
726 static_assert(NextToken(" ='date'", '=', TrimType::Name) ==
727  std::pair<std::string_view, std::optional<std::string_view::size_type>>(" ", 9));
728 #endif
729 } // namespace daq::fits
daq::fits::LiteralKeyword::GetRecord
std::string_view GetRecord() const &noexcept
Definition: keyword.cpp:414
daq::fits::LiteralKeyword::LiteralKeyword
LiteralKeyword() noexcept
Initializes an empty record (filled with ' ' characters)
Definition: keyword.cpp:363
daq::fits::KeywordVariant
std::variant< ValueKeyword, EsoKeyword, LiteralKeyword > KeywordVariant
The different variants of keywords that are supported.
Definition: keyword.hpp:400
daq::fits::LiteralKeyword::Components::value
std::string_view value
Definition: keyword.hpp:134
daq::fits::operator<<<EsoKeywordTraits >
template std::ostream & operator<<<EsoKeywordTraits >(std::ostream &os, BasicKeyword< EsoKeywordTraits > const &kw)
daq::fits::operator<<
std::ostream & operator<<(std::ostream &os, LiteralKeyword const &kw)
Definition: keyword.cpp:431
daq::fits::LiteralKeyword::operator=
LiteralKeyword & operator=(LiteralKeyword const &other) noexcept
Definition: keyword.cpp:397
daq::fits::v1::StandardSort
void StandardSort(std::vector< LiteralKeyword > &keywords)
Sorts keywords according to ESO DICD standards.
Definition: keyword.cpp:716
daq::fits::operator<
bool operator<(LiteralKeyword const &, LiteralKeyword const &) noexcept
Sort by logical keyword name (not DICD sort)
Definition: keyword.cpp:419
daq::fits::KeywordNameView
Definition: keyword.hpp:79
daq::fits::KeywordType
KeywordType
Type of FITS keyword.
Definition: keyword.hpp:64
daq::fits::BasicKeyword::BasicKeyword
BasicKeyword()=default
keyword.hpp
Contains data structure for FITS keywords.
daq::fits::NameEquals
bool NameEquals(KeywordVariant const &lhs, KeywordVariant const &rhs) noexcept
Compare logical keyword names of keyword of the same type.
Definition: keyword.cpp:477
daq::fits::Eso
@ Eso
An ESO hiearchical keyword.
Definition: keyword.hpp:72
daq::fits::BasicKeyword::GetName
constexpr KeywordNameView GetName() const &noexcept
Query logical keyword name.
Definition: keyword.hpp:285
daq::fits::BasicKeywordBase::ValueType
std::variant< std::string, int64_t, uint64_t, double, bool > ValueType
Definition: keyword.hpp:243
daq::fits::InsertKeywords
void InsertKeywords(KeywordVector &keywords, KeywordVector::iterator position, KeywordVector::const_iterator from_first, KeywordVector::const_iterator from_last)
Insert keywords.
Definition: keyword.cpp:569
daq::fits
Definition: cfitsio.cpp:14
daq::fits::LiteralKeyword::Components::type
KeywordType type
Definition: keyword.hpp:131
daq::fits::LiteralKeyword::Components
Decomposed components a literal keyword.
Definition: keyword.hpp:130
daq::fits::BasicKeyword::operator<
bool operator<(BasicKeyword const &rhs) const noexcept
Uses name property as the sorting index.
Definition: keyword.cpp:473
daq::fits::GetKeywordClass
KeywordClass GetKeywordClass(std::string_view name)
Get keyword class.
Definition: keyword.cpp:489
daq::fits::Format
LiteralKeyword Format(KeywordVariant const &keyword)
Definition: keyword.cpp:622
daq::fits::BasicKeyword
A type safe version of LiteralKeyword that consist of the three basic components of a FITS keyword ke...
Definition: keyword.hpp:266
daq::fits::LiteralKeyword::GetName
constexpr KeywordNameView GetName() const &noexcept
Query logical keyword name.
Definition: keyword.hpp:186
daq::fits::LiteralKeyword::Components::name
std::string_view name
Definition: keyword.hpp:133
daq::fits::BasicKeyword::comment
std::optional< std::string > comment
Trimmed keyword comment.
Definition: keyword.hpp:311
daq::fits::LiteralKeyword::Components::comment
std::string_view comment
Comment may be empty.
Definition: keyword.hpp:138
daq::fits::operator==
bool operator==(KeywordNameView lhs, KeywordNameView rhs) noexcept
Definition: keyword.cpp:355
daq::fits::Commentary
@ Commentary
A commentary keyword, which are keywords that do not fall into the previous categories.
Definition: keyword.hpp:76
daq::fits::KeywordClass
KeywordClass
Fits keyword type.
Definition: keyword.hpp:89
daq::fits::BasicKeyword::value
ValueType value
Definition: keyword.hpp:307
daq::fits::v1::StandardLess
Sorting function object.
Definition: keyword.hpp:533
daq::fits::operator!=
bool operator!=(KeywordNameView lhs, KeywordNameView rhs) noexcept
Definition: keyword.cpp:359
daq::fits::UpdateKeywords
void UpdateKeywords(KeywordVector &to, KeywordVector const &from, ConflictPolicy policy=ConflictPolicy::Replace)
Updates to with keywords from from.
Definition: keyword.cpp:553
daq::fits::operator<<<ValueKeywordTraits >
template std::ostream & operator<<<ValueKeywordTraits >(std::ostream &os, BasicKeyword< ValueKeywordTraits > const &kw)
daq::fits::KeywordVector
std::vector< KeywordVariant > KeywordVector
Vector of keywords.
Definition: keyword.hpp:414
daq::fits::KeywordNameView::type
KeywordType type
Definition: keyword.hpp:81
daq::fits::Value
@ Value
A value keyword.
Definition: keyword.hpp:68
daq::fits::ConflictPolicy
ConflictPolicy
Definition: keyword.hpp:463
daq::fits::BasicKeyword::operator!=
bool operator!=(BasicKeyword const &rhs) const noexcept
Compares all members for inequality.
Definition: keyword.cpp:468
daq::fits::LiteralKeyword
Represents the literal 80-character FITS keyword record.
Definition: keyword.hpp:125
daq::fits::BasicKeyword::name
std::string name
Trimmed keyword name.
Definition: keyword.hpp:306
daq::fits::BasicKeyword::operator==
bool operator==(BasicKeyword const &rhs) const noexcept
Compares all members for equality.
Definition: keyword.cpp:463
daq::fits::KeywordNameView::name
std::string_view name
Definition: keyword.hpp:80