Added performers. fixes #278
This commit is contained in:
@@ -132,6 +132,48 @@ getArtists(const TagLib::PropertyMap& properties,
|
||||
return artists;
|
||||
}
|
||||
|
||||
static
|
||||
PerformerContainer
|
||||
getPerformerArtists(const TagLib::PropertyMap& properties,
|
||||
const std::vector<std::string_view>& artistTagNames)
|
||||
{
|
||||
PerformerContainer performers;
|
||||
|
||||
// picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
|
||||
// PERFORMER: artist (role)
|
||||
if (const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(properties, artistTagNames)}; !artistNames.empty())
|
||||
{
|
||||
for (std::string_view entry : artistNames)
|
||||
{
|
||||
Utils::PerformerArtist performer {Utils::extractPerformerAndRole(entry)};
|
||||
StringUtils::capitalize(performer.role);
|
||||
performers[performer.role].push_back(std::move(performer.artist));
|
||||
}
|
||||
}
|
||||
// PERFORMER:role (MP3)
|
||||
else
|
||||
{
|
||||
for (const auto& [key, values] : properties)
|
||||
{
|
||||
if (key.startsWith("PERFORMER"))
|
||||
{
|
||||
std::string performerStr {key.to8Bit(true)};
|
||||
std::string role;
|
||||
if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos)
|
||||
{
|
||||
role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
|
||||
StringUtils::capitalize(role);
|
||||
}
|
||||
|
||||
for (const auto& value : values)
|
||||
performers[role].push_back(Artist {value.to8Bit(true)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return performers;
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<Album>
|
||||
getAlbum(const TagLib::PropertyMap& properties)
|
||||
@@ -140,7 +182,7 @@ getAlbum(const TagLib::PropertyMap& properties)
|
||||
if (albumName.empty())
|
||||
return std::nullopt;
|
||||
|
||||
const std::vector<UUID> albumMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID"})};
|
||||
const std::vector<UUID> albumMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"})};
|
||||
|
||||
if (albumMBID.empty())
|
||||
return Album {std::move(albumName.front()), {}};
|
||||
@@ -187,12 +229,14 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|
||||
if (tag == "TITLE")
|
||||
track.title = value;
|
||||
else if (tag == "MUSICBRAINZ_RELEASETRACKID"
|
||||
|| tag == "MUSICBRAINZ RELEASE TRACK ID")
|
||||
|| tag == "MUSICBRAINZ RELEASE TRACK ID"
|
||||
|| tag == "MUSICBRAINZ/RELEASE TRACK ID")
|
||||
{
|
||||
track.trackMBID = UUID::fromString(value);
|
||||
}
|
||||
else if (tag == "MUSICBRAINZ_TRACKID"
|
||||
|| tag == "MUSICBRAINZ TRACK ID")
|
||||
|| tag == "MUSICBRAINZ TRACK ID"
|
||||
|| tag == "MUSICBRAINZ/TRACK ID")
|
||||
track.recordingMBID = UUID::fromString(value);
|
||||
else if (tag == "ACOUSTID_ID")
|
||||
track.acoustID = UUID::fromString(value);
|
||||
@@ -416,23 +460,19 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
track.hasCover = true;
|
||||
}
|
||||
|
||||
for (const auto& property : properties)
|
||||
{
|
||||
const std::string tag {property.first.upper().to8Bit(true)};
|
||||
const TagLib::StringList& values {property.second};
|
||||
|
||||
processTag(track, tag, values, debug);
|
||||
}
|
||||
for (const auto& [tag, values] : properties)
|
||||
processTag(track, tag.upper().to8Bit(true), values, debug);
|
||||
|
||||
track.album = getAlbum(properties);
|
||||
track.artists = getArtists(properties, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"});
|
||||
track.albumArtists = getArtists(properties, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID"});
|
||||
track.artists = getArtists(properties, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID"});
|
||||
track.albumArtists = getArtists(properties, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"});
|
||||
track.conductorArtists = getArtists(properties, {"CONDUCTORS", "CONDUCTOR"}, {"CONDUCTORSSORT", "CONDUCTORSORT"}, {});
|
||||
track.composerArtists = getArtists(properties, {"COMPOSERS", "COMPOSER"}, {"COMPOSERSSORT", "COMPOSERSORT"}, {});
|
||||
track.lyricistArtists = getArtists(properties, {"LYRICISTS", "LYRICIST"}, {"LYRICISTSSORT", "LYRICISTSORT"}, {});
|
||||
track.mixerArtists = getArtists(properties, {"MIXERS", "MIXER"}, {"MIXERSSORT", "MIXERSORT"}, {});
|
||||
track.producerArtists = getArtists(properties, {"PRODUCERS", "PRODUCER"}, {"PRODUCERSSORT", "PRODUCERSORT"}, {});
|
||||
track.remixerArtists = getArtists(properties, {"REMIXERS", "REMIXER", "ModifiedBy"}, {"REMIXERSSORT", "REMIXERSORT"}, {});
|
||||
track.performerArtists = getPerformerArtists(properties, {"PERFORMERS", "PERFORMER"});
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
@@ -71,5 +71,51 @@ namespace MetaData::Utils
|
||||
|
||||
throw LmsException {"Unknown read style"};
|
||||
}
|
||||
|
||||
PerformerArtist
|
||||
extractPerformerAndRole(std::string_view entry)
|
||||
{
|
||||
std::string_view artistName;
|
||||
std::string_view role;
|
||||
|
||||
std::size_t roleBegin {};
|
||||
std::size_t roleEnd {};
|
||||
std::size_t count {};
|
||||
|
||||
for (std::size_t i {}; i < entry.size(); ++i)
|
||||
{
|
||||
std::size_t currentIndex {entry.size() - i - 1};
|
||||
const char c {entry[currentIndex]};
|
||||
|
||||
if (std::isspace(c))
|
||||
continue;
|
||||
|
||||
if (c == ')')
|
||||
{
|
||||
if (count++ == 0)
|
||||
roleEnd = currentIndex;
|
||||
}
|
||||
else if (c == '(')
|
||||
{
|
||||
if (count == 0)
|
||||
break;
|
||||
|
||||
if (--count == 0)
|
||||
{
|
||||
roleBegin = currentIndex + 1;
|
||||
role = StringUtils::stringTrim(entry.substr(roleBegin, roleEnd - roleBegin));
|
||||
artistName = StringUtils::stringTrim(entry.substr(0, currentIndex));
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (count == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!roleEnd || !roleBegin)
|
||||
artistName = StringUtils::stringTrim(entry);
|
||||
|
||||
return PerformerArtist {Artist {artistName}, std::string {role}};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <string_view>
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
#include "metadata/IParser.hpp"
|
||||
@@ -26,5 +27,14 @@ namespace MetaData::Utils
|
||||
{
|
||||
Wt::WDate parseDate(const std::string& dateStr);
|
||||
std::string_view readStyleToString(ParserReadStyle readStyle);
|
||||
|
||||
struct PerformerArtist
|
||||
{
|
||||
Artist artist;
|
||||
std::string role;
|
||||
};
|
||||
|
||||
// format is "artist name (role)"
|
||||
PerformerArtist extractPerformerAndRole(std::string_view entry);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
@@ -44,6 +45,8 @@ namespace MetaData
|
||||
Artist(std::string_view _name, std::optional<std::string> _sortName, std::optional<UUID> _musicBrainzArtistID) : name {_name}, sortName {_sortName}, musicBrainzArtistID {_musicBrainzArtistID} {}
|
||||
};
|
||||
|
||||
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
|
||||
|
||||
struct Album
|
||||
{
|
||||
std::string name;
|
||||
@@ -83,6 +86,7 @@ namespace MetaData
|
||||
std::vector<Artist> composerArtists;
|
||||
std::vector<Artist> lyricistArtists;
|
||||
std::vector<Artist> mixerArtists;
|
||||
PerformerContainer performerArtists;
|
||||
std::vector<Artist> producerArtists;
|
||||
std::vector<Artist> remixerArtists;
|
||||
};
|
||||
|
||||
@@ -62,7 +62,6 @@ TEST(MetaData, parseDate)
|
||||
{ "1995-05/09", Wt::WDate {} }, // invalid mixup separators
|
||||
};
|
||||
|
||||
|
||||
for (const TestCase& testCase : testCases)
|
||||
{
|
||||
const Wt::WDate parsed {parseDate(testCase.str)};
|
||||
@@ -72,3 +71,47 @@ TEST(MetaData, parseDate)
|
||||
EXPECT_EQ(parsed.day(), testCase.result.day()) << " str was '" << testCase.str << "'";
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MetaData, extractPerformerAndRole)
|
||||
{
|
||||
using namespace MetaData::Utils;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string str;
|
||||
std::string expectedArtistName;
|
||||
std::string expectedRole;
|
||||
} testCases []
|
||||
{
|
||||
{ "", "", "" },
|
||||
{ "(myrole)", "", "myrole" },
|
||||
{ "(my role)", "", "my role" },
|
||||
{ " ( my role ) ", "", "my role" },
|
||||
{ " (()) ", "", "()" },
|
||||
{ ")", ")", "" },
|
||||
{ "(", "(", "" },
|
||||
{ "artist name (my role)", "artist name", "my role" },
|
||||
{ "artist name ()", "artist name", "" },
|
||||
{ "artist name ( )", "artist name", "" },
|
||||
{ "artist (subname) name", "artist (subname) name", "" },
|
||||
{ " artist name ( my role )", "artist name", "my role" },
|
||||
{ "artist name (artist subname) (my role)", "artist name (artist subname)", "my role" },
|
||||
{ "artist name", "artist name", "" },
|
||||
{ " artist name ", "artist name", "" },
|
||||
{ "artist name (", "artist name (", "" },
|
||||
{ "artist name )", "artist name )", "" },
|
||||
{ "artist name (()", "artist name (", "" },
|
||||
{ "artist name (())", "artist name", "()" },
|
||||
{ "artist name ( () )", "artist name", "()" },
|
||||
{ "artist name (drums (drum set))", "artist name", "drums (drum set)" },
|
||||
{ "artist name ( drums (drum set) )", "artist name", "drums (drum set)" },
|
||||
};
|
||||
|
||||
for (const TestCase& testCase : testCases)
|
||||
{
|
||||
PerformerArtist performer {extractPerformerAndRole(testCase.str)};
|
||||
|
||||
EXPECT_EQ(performer.artist.name, testCase.expectedArtistName) << " str was '" << testCase.str << "'";
|
||||
EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,6 +600,17 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV37(Session& session)
|
||||
{
|
||||
// Support Performer tags (via subtypes)
|
||||
session.getDboSession().execute("ALTER TABLE track_artist_link ADD subtype TEXT");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
void
|
||||
doDbMigration(Session& session)
|
||||
{
|
||||
@@ -643,6 +654,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
||||
{34, migrateFromV34},
|
||||
{35, migrateFromV35},
|
||||
{36, migrateFromV36},
|
||||
{37, migrateFromV37},
|
||||
};
|
||||
|
||||
while (1)
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Database
|
||||
class Session;
|
||||
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION {37};
|
||||
static constexpr Version LMS_DATABASE_VERSION {38};
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -24,36 +24,78 @@
|
||||
#include "services/database/Track.hpp"
|
||||
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type)
|
||||
: _type {type},
|
||||
_track {getDboPtr(track)},
|
||||
_artist {getDboPtr(artist)}
|
||||
namespace Database
|
||||
{
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer
|
||||
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
TrackArtistLink::pointer res {session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
EnumSet<TrackArtistLinkType>
|
||||
TrackArtistLink::findUsedTypes(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList()};
|
||||
|
||||
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<TrackArtistLinkId>
|
||||
createQuery(Session& session, const TrackArtistLink::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<TrackArtistLinkId>("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l")};
|
||||
|
||||
if (params.linkType)
|
||||
query.where("t_a_l.type = ?").bind(*params.linkType);
|
||||
|
||||
if (params.track.isValid() || params.release.isValid())
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
: _type {type}
|
||||
, _subType {subType}
|
||||
, _track {getDboPtr(track)}
|
||||
, _artist {getDboPtr(artist)}
|
||||
{
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer
|
||||
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
TrackArtistLink::pointer res {session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer
|
||||
TrackArtistLink::find(Session& session, TrackArtistLinkId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<TrackArtistLinkId>
|
||||
TrackArtistLink::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery(session, params)};
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
|
||||
EnumSet<TrackArtistLinkType>
|
||||
TrackArtistLink::findUsedTypes(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList()};
|
||||
|
||||
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@ class Artist : public Object<Artist, ArtistId>
|
||||
static RangeResults<ArtistId> findAllOrphans(Session& session, Range range); // No track related
|
||||
static bool exists(Session& session, ArtistId id);
|
||||
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
|
||||
@@ -20,11 +20,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "services/database/IdType.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/TrackId.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
|
||||
@@ -40,31 +43,48 @@ namespace Database
|
||||
class TrackArtistLink : public Object<TrackArtistLink, TrackArtistLinkId>
|
||||
{
|
||||
public:
|
||||
TrackArtistLink() = default;
|
||||
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
|
||||
struct FindParameters
|
||||
{
|
||||
Range range;
|
||||
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
|
||||
TrackId track; // artists involved in this track
|
||||
ReleaseId release; // artists involved in this release
|
||||
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
|
||||
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
||||
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
|
||||
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
};
|
||||
|
||||
TrackArtistLink() = default;
|
||||
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType);
|
||||
|
||||
static RangeResults<TrackArtistLinkId> find(Session& session, const FindParameters& parameters);
|
||||
static pointer find(Session& session, TrackArtistLinkId linkId);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {});
|
||||
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
|
||||
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||
TrackArtistLinkType getType() const { return _type; }
|
||||
std::string_view getSubType() const { return _subType; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _subType, "subtype");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
TrackArtistLinkType _type;
|
||||
std::string _subType;
|
||||
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -837,6 +837,12 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan,
|
||||
for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists, true))
|
||||
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, mixer, TrackArtistLinkType::Mixer));
|
||||
|
||||
for (const auto& [role, performers] : trackInfo->performerArtists)
|
||||
{
|
||||
for (const Artist::pointer& performer : getOrCreateArtists(_dbSession, performers, true))
|
||||
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, performer, TrackArtistLinkType::Performer, role));
|
||||
}
|
||||
|
||||
for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists, true))
|
||||
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, producer, TrackArtistLinkType::Producer));
|
||||
|
||||
|
||||
@@ -115,17 +115,21 @@ joinStrings(const std::vector<std::string>& strings, const std::string& delimite
|
||||
return boost::algorithm::join(strings, delimiter);
|
||||
}
|
||||
|
||||
std::string
|
||||
std::string_view
|
||||
stringTrim(std::string_view str, std::string_view whitespaces)
|
||||
{
|
||||
std::string_view res;
|
||||
|
||||
const auto strBegin = str.find_first_not_of(whitespaces);
|
||||
if (strBegin == std::string_view::npos)
|
||||
return ""; // no content
|
||||
if (strBegin != std::string_view::npos)
|
||||
{
|
||||
const auto strEnd {str.find_last_not_of(whitespaces)};
|
||||
const auto strRange {strEnd - strBegin + 1};
|
||||
|
||||
const auto strEnd = str.find_last_not_of(whitespaces);
|
||||
const auto strRange = strEnd - strBegin + 1;
|
||||
res = str.substr(strBegin, strRange);
|
||||
}
|
||||
|
||||
return std::string {str.substr(strBegin, strRange)};
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string
|
||||
@@ -175,6 +179,21 @@ bufferToString(const std::vector<unsigned char>& data)
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
void
|
||||
capitalize(std::string& str)
|
||||
{
|
||||
for (auto it {std::begin(str)}; it != std::end(str); ++it)
|
||||
{
|
||||
if (std::isspace(*it))
|
||||
continue;
|
||||
|
||||
if (std::isalpha(*it))
|
||||
*it = std::toupper(*it);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string
|
||||
replaceInString(std::string_view str, const std::string& from, const std::string& to)
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@ std::string
|
||||
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
std::string_view
|
||||
stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
@@ -66,6 +66,9 @@ stringToUpper(const std::string& str);
|
||||
std::string
|
||||
bufferToString(const std::vector<unsigned char>& data);
|
||||
|
||||
void
|
||||
capitalize(std::string& str);
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]]
|
||||
std::optional<T> readAs(std::string_view str)
|
||||
|
||||
@@ -118,3 +118,32 @@ TEST(StringUtils, readAs)
|
||||
EXPECT_EQ(StringUtils::readAs<bool>("foo"), std::nullopt);
|
||||
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
|
||||
}
|
||||
|
||||
TEST(StrinUtils, capitalize)
|
||||
{
|
||||
struct TestCase
|
||||
{
|
||||
std::string input;
|
||||
std::string expectedOutput;
|
||||
};
|
||||
|
||||
TestCase tests[]
|
||||
{
|
||||
{"", ""},
|
||||
{"C", "C"},
|
||||
{"c", "C"},
|
||||
{" c", " C"},
|
||||
{" cc", " Cc"},
|
||||
{"(c", "(c"},
|
||||
{"1c", "1c"},
|
||||
{"&c", "&c"},
|
||||
{"c c", "C c"}
|
||||
};
|
||||
|
||||
for (const TestCase& test : tests)
|
||||
{
|
||||
std::string str {test.input};
|
||||
StringUtils::capitalize(str);
|
||||
EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user