Skip to content

Commit

Permalink
add - doc - Added conversion from vCard to MeCard
Browse files Browse the repository at this point in the history
---

We've added a way to convert vCard to MeCard!

---

Type: add
Breaking: False
Doc Required: True
Part: 1/1
  • Loading branch information
AptiviCEO committed Apr 3, 2024
1 parent db841aa commit 74afa8f
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 16 deletions.
68 changes: 68 additions & 0 deletions VisualCard.Tests/ContactData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ public static class ContactData
"""
;

private static readonly string singleMeCardContactShortReparsed =
"""
MECARD:N:Hood,Rick;;
"""
;

private static readonly string singleMeCardContactShortReparsedCompatibility =
"""
MECARD:N:Hood,Rick;;
"""
;

private static readonly string singleVcardContactShortFromMeCard =
"""
BEGIN:VCARD
Expand Down Expand Up @@ -101,6 +113,30 @@ public static class ContactData
"""
;

private static readonly string singleMeCardContactReparsed =
"""
MECARD:N:Sanders,John;TEL:495-522-3560;EMAIL:john.s@acme.co;NOTE:Note test for VisualCard;ADR:,,Los Angeles,,,,USA;;
"""
;

private static readonly string singleMeCardContactFullReparsed =
"""
MECARD:N:Sanders,John;SOUND:Saunders,John;TEL:495-522-3560;TEL-AV:495-522-3550;EMAIL:john.s@acme.co;NOTE:Note test for VisualCard;ADR:,,Los Angeles,,,,USA;;
"""
;

private static readonly string singleMeCardContactReparsedCompatibility =
"""
MECARD:N:Sanders,John;TEL:495-522-3560;EMAIL:john.s@acme.co;ADR:,,Los Angeles,,,,USA;;
"""
;

private static readonly string singleMeCardContactFullReparsedCompatibility =
"""
MECARD:N:Sanders,John;SOUND:Saunders,John;TEL:495-522-3560;EMAIL:john.s@acme.co;ADR:,,Los Angeles,,,,USA;;
"""
;

private static readonly string singleVcardContactFromMeCard =
"""
BEGIN:VCARD
Expand Down Expand Up @@ -585,6 +621,38 @@ public static class ContactData
],
];

/// <summary>
/// Test MeCard contacts
/// </summary>
public static IEnumerable<object[]> meCardContactsReparsed =>
[
[
(singleMeCardContactShort, singleMeCardContactShortReparsed),
],
[
(singleMeCardContact, singleMeCardContactReparsed),
],
[
(singleMeCardContactFull, singleMeCardContactFullReparsed),
],
];

/// <summary>
/// Test MeCard contacts
/// </summary>
public static IEnumerable<object[]> meCardContactsReparsedCompatibility =>
[
[
(singleMeCardContactShort, singleMeCardContactShortReparsedCompatibility),
],
[
(singleMeCardContact, singleMeCardContactReparsedCompatibility),
],
[
(singleMeCardContactFull, singleMeCardContactFullReparsedCompatibility),
],
];

/// <summary>
/// Test MeCard contacts
/// </summary>
Expand Down
31 changes: 31 additions & 0 deletions VisualCard.Tests/ContactParseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,37 @@ public void ParseDifferentMeCardContacts(string cardText)
Should.NotThrow(() => cards = MeCard.GetContactsFromMeCardString(cardText));
}

[TestMethod]
[DynamicData(nameof(ContactData.meCardContactsReparsed), typeof(ContactData))]
public void ParseDifferentMeCardContactsAndReparse((string, string) cardText)
{
Card[] cards = [];
Card[] secondCards = [];
Should.NotThrow(() => cards = MeCard.GetContactsFromMeCardString(cardText.Item1));
foreach (var card in cards)
{
string meCardSaved = MeCard.SaveCardToMeCardString(card);
meCardSaved.ShouldBe(cardText.Item2);
Should.NotThrow(() => secondCards = MeCard.GetContactsFromMeCardString(meCardSaved));
secondCards[0].ShouldBe(card);
}
}

[TestMethod]
[DynamicData(nameof(ContactData.meCardContactsReparsedCompatibility), typeof(ContactData))]
public void ParseDifferentMeCardContactsAndReparseCompatibility((string, string) cardText)
{
Card[] cards = [];
Card[] secondCards = [];
Should.NotThrow(() => cards = MeCard.GetContactsFromMeCardString(cardText.Item1));
foreach (var card in cards)
{
string meCardSaved = MeCard.SaveCardToMeCardString(card, true);
meCardSaved.ShouldBe(cardText.Item2);
Should.NotThrow(() => secondCards = MeCard.GetContactsFromMeCardString(meCardSaved));
}
}

[TestMethod]
[DynamicData(nameof(ContactData.meCardContacts), typeof(ContactData))]
public void ParseDifferentMeCardContactsAndTestEquality(string cardText)
Expand Down
185 changes: 169 additions & 16 deletions VisualCard/Converters/MeCard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
using System;
using VisualCard.Parsers;
using VisualCard.Parts;
using VisualCard.Parts.Implementations;
using VisualCard.Parts.Enums;
using System.Linq;
using System.Collections.Generic;

namespace VisualCard.Converters
{
Expand All @@ -30,6 +34,23 @@ namespace VisualCard.Converters
/// </summary>
public static class MeCard
{
private const string _meCardBegin = "MECARD:";
private const string _meCardEnd = ";;";
private const char _meCardArgumentDelimiter = ':';
private const char _meCardFieldDelimiter = ';';
private const char _meCardValueDelimiter = ',';
private const string _meCardNameSpecifier = "N";
private const string _meCardSoundSpecifier = "SOUND";
private const string _meCardTelephoneSpecifier = "TEL";
private const string _meCardVideophoneSpecifier = "TEL-AV";
private const string _meCardEmailSpecifier = "EMAIL";
private const string _meCardNoteSpecifier = "NOTE";
private const string _meCardBirthdaySpecifier = "BDAY";
private const string _meCardAddressSpecifier = "ADR";
private const string _meCardUrlSpecifier = "URL";
private const string _meCardNicknameSpecifier = "NICKNAME";
private const string _meCardXNameKanaSpecifier = "VISUALCARD-KANA";

/// <summary>
/// Gets all contacts from a MeCard string
/// </summary>
Expand All @@ -42,17 +63,19 @@ public static Card[] GetContactsFromMeCardString(string meCardString)
// Check to see if the MeCard string is valid
if (string.IsNullOrWhiteSpace(meCardString))
throw new InvalidDataException("MeCard string should not be empty.");
if (!meCardString.StartsWith("MECARD:") && !meCardString.EndsWith(";;"))
if (!meCardString.StartsWith(_meCardBegin) && !meCardString.EndsWith(_meCardEnd))
throw new InvalidDataException("This string doesn't represent a valid MeCard contact.");

// Now, parse it.
try
{
// Split the meCard string from the beginning and the ending
meCardString = meCardString.Substring(meCardString.IndexOf(":") + 1, meCardString.IndexOf(";;") - (meCardString.IndexOf(":") + 1));
int beginningIdx = meCardString.IndexOf(_meCardArgumentDelimiter) + 1;
int endingIdx = meCardString.IndexOf(_meCardEnd) - beginningIdx;
meCardString = meCardString.Substring(beginningIdx, endingIdx);

// Split the values from the semicolons
var values = meCardString.Split(';');
var values = meCardString.Split(_meCardFieldDelimiter);
string fullName = "";

// Replace all the commas found with semicolons if possible
Expand All @@ -61,31 +84,31 @@ public static Card[] GetContactsFromMeCardString(string meCardString)
string value = values[i];

// "SOUND:" here is actually just a Kana name, so demote it to X-nonstandard
if (value.StartsWith($"{VcardConstants._soundSpecifier}:"))
if (value.StartsWith($"{_meCardSoundSpecifier}{_meCardArgumentDelimiter}"))
{
string xNonstandard = $"{VcardConstants._xSpecifier}VISUALCARD-KANA:";
string xNonstandard = $"{VcardConstants._xSpecifier}{_meCardXNameKanaSpecifier}{_meCardArgumentDelimiter}";
values[i] = value.Replace(",", ";");
values[i] = xNonstandard + values[i].Substring(6);
}

// Now, replace all the commas in Name and Address with the semicolons.
if (value.StartsWith($"{VcardConstants._nameSpecifier}:") || value.StartsWith($"{VcardConstants._addressSpecifier}:"))
if (value.StartsWith($"{_meCardNameSpecifier}{_meCardArgumentDelimiter}") || value.StartsWith($"{_meCardAddressSpecifier}{_meCardArgumentDelimiter}"))
values[i] = value.Replace(",", ";");

// Build a full name
if (value.StartsWith($"{VcardConstants._nameSpecifier}:"))
if (value.StartsWith($"{_meCardNameSpecifier}{_meCardArgumentDelimiter}"))
{
var nameSplits = value.Substring(2).Split(',');
fullName = $"{nameSplits[1]} {nameSplits[0]}";
}

// "TEL-AV:" here is actually just "TEL;TYPE=VIDEO:[...]"
if (value.StartsWith($"{VcardConstants._telephoneSpecifier}-AV:"))
if (value.StartsWith($"{_meCardVideophoneSpecifier}{_meCardArgumentDelimiter}"))
{
string prefix =
$"{VcardConstants._telephoneSpecifier}" +
$"{VcardConstants._fieldDelimiter}" +
$"{VcardConstants._typeArgumentSpecifier}VIDEO:";
$"{VcardConstants._typeArgumentSpecifier}VIDEO{VcardConstants._argumentDelimiter}";
values[i] = prefix + values[i].Substring(7);
}
}
Expand All @@ -94,15 +117,15 @@ public static Card[] GetContactsFromMeCardString(string meCardString)
var masterContactBuilder = new StringBuilder(
$"""
{VcardConstants._beginText}
{VcardConstants._versionSpecifier}:3.0
{VcardConstants._versionSpecifier}{VcardConstants._argumentDelimiter}3.0

"""
);
foreach (var value in values)
masterContactBuilder.AppendLine(value);
masterContactBuilder.AppendLine(
$"""
{VcardConstants._fullNameSpecifier}:{fullName}
{VcardConstants._fullNameSpecifier}{VcardConstants._argumentDelimiter}{fullName}
{VcardConstants._endText}
"""
);
Expand All @@ -115,16 +138,146 @@ public static Card[] GetContactsFromMeCardString(string meCardString)
throw new InvalidDataException("The MeCard contact string is not valid.", ex);
}
}

/// <summary>
/// Saves the vCard <see cref="Card"/> instance to a MeCard string for QR code generation
/// </summary>
/// <param name="card">Card instance</param>
/// <returns>a MeCard string</returns>
public static string SaveCardToMeCardString(Card card)
/// <param name="compatibility">Compatibility mode (excludes some MeCard values)</param>
/// <returns>A MeCard string</returns>
public static string SaveCardToMeCardString(Card card, bool compatibility = false)
{
// TODO: Scaffolding. Please implment by version 1.0.0.
return "";
// Check the card for validity
if (card == null)
throw new ArgumentNullException(nameof(card), "Card is not provided.");

// Now, get all the values in the below order
var names = card.GetPartsArray<NameInfo>();
var fullName = card.GetString(StringsEnum.FullName);
var xNames = card.GetPartsArray<XNameInfo>();
var telephones = card.GetPartsArray<TelephoneInfo>();
var emails = card.GetPartsArray<EmailInfo>();
var note = card.GetString(StringsEnum.Notes);
var birthday = card.GetPart<BirthDateInfo>();
var addresses = card.GetPartsArray<AddressInfo>();
var url = card.GetString(StringsEnum.Url);
var nicknames = card.GetPartsArray<NicknameInfo>();

// Check them for existence
bool hasNames = names.Length > 0;
bool hasFullName = !string.IsNullOrEmpty(fullName);
bool hasReading = xNames.Any((xName) => xName.XKeyName == _meCardXNameKanaSpecifier);
bool hasTelephone = telephones.Length > 0 && telephones.Any((tel) => !tel.HasType("video"));
bool hasVideophone = telephones.Length > 0 && telephones.Any((tel) => tel.HasType("video")) && !compatibility;
bool hasEmails = emails.Length > 0;
bool hasNote = !string.IsNullOrEmpty(note) && !compatibility;
bool hasBirthday = birthday is not null;
bool hasAddresses = addresses.Length > 0;
bool hasUrl = !string.IsNullOrEmpty(url) && !compatibility;
bool hasNicknames = nicknames.Length > 0 && !compatibility;
if (!hasNames && !hasFullName)
throw new InvalidDataException("Can't build a MeCard string from a vCard containing an empty name or an empty full name.");

// Add the types
List<string> properties = [];
if (hasNames)
{
StringBuilder builder = new();
var name = names[0];
builder.Append(_meCardNameSpecifier + _meCardArgumentDelimiter);
builder.Append(name.ContactLastName + _meCardValueDelimiter);
builder.Append(name.ContactFirstName);
properties.Add(builder.ToString());
}
else if (hasFullName)
{
StringBuilder builder = new();
string[] splitFullName = fullName.Split([" "], StringSplitOptions.RemoveEmptyEntries);
builder.Append(_meCardNameSpecifier + _meCardArgumentDelimiter);
builder.Append(string.Join(_meCardValueDelimiter.ToString(), splitFullName));
properties.Add(builder.ToString());
}
if (hasReading)
{
StringBuilder builder = new();
var kana = xNames.First((xName) => xName.XKeyName == _meCardXNameKanaSpecifier);
builder.Append(_meCardSoundSpecifier + _meCardArgumentDelimiter);
builder.Append(string.Join(_meCardValueDelimiter.ToString(), kana.XValues));
properties.Add(builder.ToString());
}
if (hasTelephone)
{
StringBuilder builder = new();
var telephone = telephones.First((tel) => !tel.HasType("video"));
builder.Append(_meCardTelephoneSpecifier + _meCardArgumentDelimiter);
builder.Append(telephone.ContactPhoneNumber);
properties.Add(builder.ToString());
}
if (hasVideophone)
{
StringBuilder builder = new();
var videophone = telephones.First((tel) => tel.HasType("video"));
builder.Append(_meCardVideophoneSpecifier + _meCardArgumentDelimiter);
builder.Append(videophone.ContactPhoneNumber);
properties.Add(builder.ToString());
}
if (hasEmails)
{
StringBuilder builder = new();
var email = emails[0];
builder.Append(_meCardEmailSpecifier + _meCardArgumentDelimiter);
builder.Append(email.ContactEmailAddress);
properties.Add(builder.ToString());
}
if (hasNote)
{
StringBuilder builder = new();
builder.Append(_meCardNoteSpecifier + _meCardArgumentDelimiter);
builder.Append(note);
properties.Add(builder.ToString());
}
if (hasBirthday)
{
StringBuilder builder = new();
builder.Append(_meCardBirthdaySpecifier + _meCardArgumentDelimiter);
builder.Append($"{birthday.BirthDate:yyyyMMdd}");
properties.Add(builder.ToString());
}
if (hasAddresses)
{
StringBuilder builder = new();
var address = addresses[0];
builder.Append(_meCardAddressSpecifier + _meCardArgumentDelimiter);
builder.Append(address.PostOfficeBox + _meCardValueDelimiter);
builder.Append(address.ExtendedAddress + _meCardValueDelimiter);
builder.Append(address.StreetAddress + _meCardValueDelimiter);
builder.Append(address.Locality + _meCardValueDelimiter);
builder.Append(address.Region + _meCardValueDelimiter);
builder.Append(address.PostalCode + _meCardValueDelimiter);
builder.Append(address.Country);
properties.Add(builder.ToString());
}
if (hasUrl)
{
StringBuilder builder = new();
builder.Append(_meCardUrlSpecifier + _meCardArgumentDelimiter);
builder.Append(url);
properties.Add(builder.ToString());
}
if (hasNicknames)
{
StringBuilder builder = new();
var nickname = nicknames[0];
builder.Append(_meCardNicknameSpecifier + _meCardArgumentDelimiter);
builder.Append(nickname.ContactNickname);
properties.Add(builder.ToString());
}

// Now, build the MeCard string
StringBuilder meCard = new(_meCardBegin);
meCard.Append(string.Join(_meCardFieldDelimiter.ToString(), properties));
meCard.Append(_meCardEnd);
return meCard.ToString();
}
}
}
Loading

0 comments on commit 74afa8f

Please sign in to comment.