#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub enum GedcomVersion {
#[default]
V5_5_1,
V7_0,
Unknown(VersionString),
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct VersionString(pub String);
impl GedcomVersion {
#[must_use]
pub fn from_version_str(version: &str) -> Self {
let version = version.trim();
if version.starts_with("7.") || version == "7" {
return GedcomVersion::V7_0;
}
if version.starts_with("5.5") || version == "5.5" {
return GedcomVersion::V5_5_1;
}
GedcomVersion::Unknown(VersionString(version.to_string()))
}
#[must_use]
pub fn is_v7(&self) -> bool {
matches!(self, GedcomVersion::V7_0)
}
#[must_use]
pub fn is_v5(&self) -> bool {
matches!(self, GedcomVersion::V5_5_1)
}
#[must_use]
pub fn is_unknown(&self) -> bool {
matches!(self, GedcomVersion::Unknown(_))
}
#[must_use]
pub fn as_str(&self) -> &str {
match self {
GedcomVersion::V5_5_1 => "5.5.1",
GedcomVersion::V7_0 => "7.0",
GedcomVersion::Unknown(s) => &s.0,
}
}
#[must_use]
pub fn supports_conc(&self) -> bool {
!self.is_v7()
}
#[must_use]
pub fn requires_utf8(&self) -> bool {
self.is_v7()
}
#[must_use]
pub fn supports_schema(&self) -> bool {
self.is_v7()
}
#[must_use]
pub fn supports_shared_notes(&self) -> bool {
self.is_v7()
}
#[must_use]
pub fn supports_submission_record(&self) -> bool {
!self.is_v7()
}
#[must_use]
pub fn supports_char_encoding(&self) -> bool {
!self.is_v7()
}
#[must_use]
pub fn doubles_all_at_signs(&self) -> bool {
!self.is_v7()
}
#[must_use]
pub fn major(&self) -> u8 {
match self {
GedcomVersion::V5_5_1 => 5,
GedcomVersion::V7_0 => 7,
GedcomVersion::Unknown(s) => {
s.0.chars()
.take_while(char::is_ascii_digit)
.collect::<String>()
.parse()
.unwrap_or(0)
}
}
}
#[must_use]
pub fn minor(&self) -> u8 {
match self {
GedcomVersion::V5_5_1 => 5,
GedcomVersion::V7_0 => 0,
GedcomVersion::Unknown(s) => {
let parts: Vec<&str> = s.0.split('.').collect();
if parts.len() > 1 {
parts[1].parse().unwrap_or(0)
} else {
0
}
}
}
}
}
impl fmt::Display for GedcomVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[must_use]
pub fn detect_version(content: &str) -> GedcomVersion {
let search_area = if content.len() > 1000 {
&content[..1000]
} else {
content
};
if let Some(gedc_pos) = search_area.find("GEDC") {
let after_gedc = &search_area[gedc_pos..];
if let Some(vers_pos) = after_gedc.find("VERS") {
let after_vers = &after_gedc[vers_pos + 4..];
let version_str: String = after_vers
.trim_start()
.chars()
.take_while(|c| !c.is_whitespace() && *c != '\n' && *c != '\r')
.collect();
if !version_str.is_empty() {
return GedcomVersion::from_version_str(&version_str);
}
}
}
GedcomVersion::V5_5_1
}
#[must_use]
pub fn appears_to_be_v7(content: &str) -> bool {
let has_bom = content.starts_with('\u{FEFF}');
let has_schema = content.contains("1 SCHMA") || content.contains("\n1 SCHMA");
let has_snote = content.contains("0 @") && content.contains("@ SNOTE");
let version = detect_version(content);
version.is_v7() || has_schema || has_snote || (has_bom && !version.is_v5())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(clippy::struct_excessive_bools)]
pub struct VersionFeatures {
pub conc_supported: bool,
pub schema_supported: bool,
pub shared_notes_supported: bool,
pub submission_supported: bool,
pub utf8_required: bool,
pub double_all_at_signs: bool,
pub char_encoding_supported: bool,
}
impl From<GedcomVersion> for VersionFeatures {
fn from(version: GedcomVersion) -> Self {
VersionFeatures {
conc_supported: version.supports_conc(),
schema_supported: version.supports_schema(),
shared_notes_supported: version.supports_shared_notes(),
submission_supported: version.supports_submission_record(),
utf8_required: version.requires_utf8(),
double_all_at_signs: version.doubles_all_at_signs(),
char_encoding_supported: version.supports_char_encoding(),
}
}
}
impl VersionFeatures {
#[must_use]
pub fn v5_5_1() -> Self {
GedcomVersion::V5_5_1.into()
}
#[must_use]
pub fn v7_0() -> Self {
GedcomVersion::V7_0.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_from_str() {
assert_eq!(
GedcomVersion::from_version_str("5.5.1"),
GedcomVersion::V5_5_1
);
assert_eq!(
GedcomVersion::from_version_str("5.5"),
GedcomVersion::V5_5_1
);
assert_eq!(
GedcomVersion::from_version_str("5.5.0"),
GedcomVersion::V5_5_1
);
assert_eq!(GedcomVersion::from_version_str("7.0"), GedcomVersion::V7_0);
assert_eq!(
GedcomVersion::from_version_str("7.0.14"),
GedcomVersion::V7_0
);
assert_eq!(GedcomVersion::from_version_str("7"), GedcomVersion::V7_0);
assert!(GedcomVersion::from_version_str("6.0").is_unknown());
}
#[test]
fn test_version_display() {
assert_eq!(GedcomVersion::V5_5_1.to_string(), "5.5.1");
assert_eq!(GedcomVersion::V7_0.to_string(), "7.0");
}
#[test]
fn test_version_features() {
let v5 = GedcomVersion::V5_5_1;
assert!(v5.supports_conc());
assert!(!v5.requires_utf8());
assert!(!v5.supports_schema());
assert!(!v5.supports_shared_notes());
assert!(v5.supports_submission_record());
assert!(v5.supports_char_encoding());
assert!(v5.doubles_all_at_signs());
let v7 = GedcomVersion::V7_0;
assert!(!v7.supports_conc());
assert!(v7.requires_utf8());
assert!(v7.supports_schema());
assert!(v7.supports_shared_notes());
assert!(!v7.supports_submission_record());
assert!(!v7.supports_char_encoding());
assert!(!v7.doubles_all_at_signs());
}
#[test]
fn test_detect_version_v5() {
let content = "0 HEAD\n1 GEDC\n2 VERS 5.5.1\n2 FORM LINEAGE-LINKED\n0 TRLR";
assert_eq!(detect_version(content), GedcomVersion::V5_5_1);
}
#[test]
fn test_detect_version_v7() {
let content = "0 HEAD\n1 GEDC\n2 VERS 7.0\n0 TRLR";
assert_eq!(detect_version(content), GedcomVersion::V7_0);
let content = "0 HEAD\n1 GEDC\n2 VERS 7.0.14\n0 TRLR";
assert_eq!(detect_version(content), GedcomVersion::V7_0);
}
#[test]
fn test_detect_version_default() {
let content = "0 HEAD\n0 TRLR";
assert_eq!(detect_version(content), GedcomVersion::V5_5_1);
}
#[test]
fn test_appears_to_be_v7() {
let v7_content = "0 HEAD\n1 GEDC\n2 VERS 7.0\n1 SCHMA\n0 TRLR";
assert!(appears_to_be_v7(v7_content));
let v5_content = "0 HEAD\n1 GEDC\n2 VERS 5.5.1\n0 TRLR";
assert!(!appears_to_be_v7(v5_content));
}
#[test]
fn test_version_major_minor() {
assert_eq!(GedcomVersion::V5_5_1.major(), 5);
assert_eq!(GedcomVersion::V5_5_1.minor(), 5);
assert_eq!(GedcomVersion::V7_0.major(), 7);
assert_eq!(GedcomVersion::V7_0.minor(), 0);
}
#[test]
fn test_version_features_struct() {
let features = VersionFeatures::v5_5_1();
assert!(features.conc_supported);
assert!(!features.utf8_required);
let features = VersionFeatures::v7_0();
assert!(!features.conc_supported);
assert!(features.utf8_required);
}
#[test]
fn test_is_predicates() {
assert!(GedcomVersion::V5_5_1.is_v5());
assert!(!GedcomVersion::V5_5_1.is_v7());
assert!(!GedcomVersion::V5_5_1.is_unknown());
assert!(!GedcomVersion::V7_0.is_v5());
assert!(GedcomVersion::V7_0.is_v7());
assert!(!GedcomVersion::V7_0.is_unknown());
let unknown = GedcomVersion::Unknown(VersionString("4.0".to_string()));
assert!(!unknown.is_v5());
assert!(!unknown.is_v7());
assert!(unknown.is_unknown());
}
}