diff --git a/autotests/mergeutiltest.cpp b/autotests/mergeutiltest.cpp index 1d4c1b6..cca94de 100644 --- a/autotests/mergeutiltest.cpp +++ b/autotests/mergeutiltest.cpp @@ -1,227 +1,234 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #define _(x) QStringLiteral(x) using namespace KItinerary; class MergeUtilTest : public QObject { Q_OBJECT private Q_SLOTS: void testIsSameReservation() { QVERIFY(!MergeUtil::isSame({}, {})); FlightReservation res1; QVERIFY(!MergeUtil::isSame(res1, {})); QVERIFY(!MergeUtil::isSame({}, res1)); res1.setReservationNumber(QLatin1String("XXX007")); Flight flight1; flight1.setFlightNumber(QLatin1String("1234")); flight1.setDepartureDay(QDate(2018, 4, 21)); res1.setReservationFor(flight1); FlightReservation res2; res2.setReservationNumber(QLatin1String("YYY008")); Flight flight2; flight2.setFlightNumber(QLatin1String("1234")); res2.setReservationFor(flight2); QVERIFY(!MergeUtil::isSame(res1, res2)); flight2.setDepartureDay(QDate(2018, 4, 21)); res2.setReservationFor(flight2); QVERIFY(!MergeUtil::isSame(res1, res2)); res2.setReservationNumber(QLatin1String("XXX007")); QVERIFY(MergeUtil::isSame(res1, res2)); } void testIsSameFlight() { Airline airline1; airline1.setIataCode(QLatin1String("KL")); Flight f1; f1.setAirline(airline1); f1.setFlightNumber(QLatin1String("8457")); f1.setDepartureTime(QDateTime(QDate(2018, 4, 2), QTime(17, 51, 0))); Flight f2; QVERIFY(!MergeUtil::isSame(f1, f2)); f2.setFlightNumber(QLatin1String("8457")); QVERIFY(!MergeUtil::isSame(f1, f2)); Airline airline2; airline2.setIataCode(QLatin1String("AF")); f2.setAirline(airline2); QVERIFY(!MergeUtil::isSame(f1, f2)); airline2.setIataCode(QLatin1String("KL")); f2.setAirline(airline2); QVERIFY(!MergeUtil::isSame(f1, f2)); f2.setDepartureDay(QDate(2018, 4, 2)); QVERIFY(MergeUtil::isSame(f1, f2)); } void testCodeShareFlight() { Airline a1; a1.setIataCode(QLatin1String("4U")); Flight f1; f1.setAirline(a1); f1.setFlightNumber(QLatin1String("42")); f1.setDepartureDay(QDate(2018, 04, 21)); Airline a2; a2.setIataCode(QLatin1String("EW")); Flight f2(f1); f2.setAirline(a2); QVERIFY(MergeUtil::isSame(f1, f2)); } void testIsSamePerson_data() { // we do not need to consider cases here that ExtractorPostprocessor eliminates for us // such as filling the full name or honoric prefixes QTest::addColumn>("data"); QTest::newRow("simple name") << QVector { {_("Volker Krause"), {}, {}}, {_("VOLKER KRAUSE"), {}, {}}, {_("VOLKER KRAUSE"), _("Volker"), _("Krause")}, {_("VOLKER KRAUSE"), {}, _("Krause")}, {_("VOLKER KRAUSE"), _("Volker"), {}}, // IATA BCBP artifacts {_("VOLKERMR KRAUSE"), _("VOLKERMR"), _("KRAUSE")}, {_("VOLKER MR KRAUSE"), _("VOLKER MR"), _("KRAUSE")} }; QTest::newRow("double family name") << QVector { {_("Andreas Cord-Landwehr"), {}, {}}, {_("ANDREAS CORD-LANDWEHR"), {}, {}}, {_("ANDREAS CORD-LANDWEHR"), _("Andreas"), _("Cord-Landwehr")}, // IATA BCBP artifacts {_("Andreas Cordlandwehr"), {}, {}}, {_("ANDREAS CORDLANDWEHR"), _("ANDREAS"), _("CORDLANDWEHR")}, {_("ANDREAS CORD LANDWEHR"), _("ANDREAS"), _("CORD LANDWEHR")}, {_("ANDREAS CORD LANDWEHR"), {}, {}} }; + QTest::newRow("diacritic") << QVector { + {_("Daniel Vrátil"), {}, {} }, + {_("Daniel Vrátil"), _("Daniel"), _("Vrátil") }, + {_("DANIEL VRATIL"), {}, {} }, + {_("DANIEL VRATIL"), _("DANIEL"), _("VRATIL") } + }; } void testIsSamePerson() { QFETCH(QVector, data); for(int i = 0; i < data.size(); ++i) { Person lhs; lhs.setName(data[i][0]); lhs.setGivenName(data[i][1]); lhs.setFamilyName(data[i][2]); for (int j = 0; j < data.size(); ++j) { Person rhs; rhs.setName(data[j][0]); rhs.setGivenName(data[j][1]); rhs.setFamilyName(data[j][2]); QVERIFY(!MergeUtil::isSamePerson(lhs, {})); QVERIFY(!MergeUtil::isSamePerson({}, lhs)); if (!MergeUtil::isSamePerson(lhs, rhs)) { qDebug() << "Left: " << lhs.name() << lhs.givenName() << lhs.familyName(); qDebug() << "Right: " << rhs.name() << rhs.givenName() << rhs.familyName(); } QVERIFY(MergeUtil::isSamePerson(lhs, rhs)); } } } void testIsNotSamePerson() { QVector data { { _("Volker Krause"), {}, {} }, { _("Andreas Cord-Landwehr"), _("Andread"), _("Cord-Landwehr") }, { _("GIVEN1 GIVEN2 FAMILY1"), {}, {} }, - { _("V K"), {}, {} } + { _("V K"), {}, {} }, + {_("Daniel Vrátil"), _("Daniel"), _("Vrátil") }, }; for(int i = 0; i < data.size(); ++i) { Person lhs; lhs.setName(data[i][0]); lhs.setGivenName(data[i][1]); lhs.setFamilyName(data[i][2]); for (int j = 0; j < data.size(); ++j) { if (i == j) { continue; } Person rhs; rhs.setName(data[j][0]); rhs.setGivenName(data[j][1]); rhs.setFamilyName(data[j][2]); QVERIFY(!MergeUtil::isSamePerson(lhs, {})); QVERIFY(!MergeUtil::isSamePerson({}, lhs)); if (MergeUtil::isSamePerson(lhs, rhs)) { qDebug() << "Left: " << lhs.name() << lhs.givenName() << lhs.familyName(); qDebug() << "Right: " << rhs.name() << rhs.givenName() << rhs.familyName(); } QVERIFY(!MergeUtil::isSamePerson(lhs, rhs)); } } } void testIsSameLodingReservation() { LodgingReservation res1; LodgingBusiness hotel1; hotel1.setName(QLatin1String("Haus Randa")); res1.setReservationFor(hotel1); res1.setCheckinTime(QDateTime(QDate(2018, 4, 9), QTime(10, 0))); res1.setReservationNumber(QLatin1String("1234")); LodgingReservation res2; QVERIFY(!MergeUtil::isSame(res1, res2)); res2.setReservationNumber(QLatin1String("1234")); QVERIFY(!MergeUtil::isSame(res1, res2)); res2.setCheckinTime(QDateTime(QDate(2018, 4, 9), QTime(15, 0))); QVERIFY(!MergeUtil::isSame(res1, res2)); LodgingBusiness hotel2; hotel2.setName(QLatin1String("Haus Randa")); res2.setReservationFor(hotel2); QVERIFY(MergeUtil::isSame(res1, res2)); } }; QTEST_APPLESS_MAIN(MergeUtilTest) #include "mergeutiltest.moc" diff --git a/src/mergeutil.cpp b/src/mergeutil.cpp index d193d67..a7f592e 100644 --- a/src/mergeutil.cpp +++ b/src/mergeutil.cpp @@ -1,427 +1,428 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "mergeutil.h" #include "logging.h" +#include "stringutil.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; /* Checks that @p lhs and @p rhs are non-empty and equal. */ static bool equalAndPresent(const QString &lhs, const QString &rhs, Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive) { return !lhs.isEmpty() && (lhs.compare(rhs, caseSensitive) == 0); } static bool equalAndPresent(const QDate &lhs, const QDate &rhs) { return lhs.isValid() && lhs == rhs; } static bool equalAndPresent(const QDateTime &lhs, const QDateTime &rhs) { return lhs.isValid() && lhs == rhs; } /* Checks that @p lhs and @p rhs are not non-equal if both values are set. */ static bool conflictIfPresent(const QString &lhs, const QString &rhs, Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive) { return !lhs.isEmpty() && !rhs.isEmpty() && lhs.compare(rhs, caseSensitive) != 0; } static bool conflictIfPresent(const QDateTime &lhs, const QDateTime &rhs) { return lhs.isValid() && rhs.isValid() && lhs != rhs; } static bool isSameFlight(const Flight &lhs, const Flight &rhs); static bool isSameTrainTrip(const TrainTrip &lhs, const TrainTrip &rhs); static bool isSameBusTrip(const BusTrip &lhs, const BusTrip &rhs); static bool isSameLodingBusiness(const LodgingBusiness &lhs, const LodgingBusiness &rhs); static bool isSameFoodEstablishment(const FoodEstablishment &lhs, const FoodEstablishment &rhs); static bool isSameTouristAttractionVisit(const TouristAttractionVisit &lhs, const TouristAttractionVisit &rhs); static bool isSameTouristAttraction(const TouristAttraction &lhs, const TouristAttraction &rhs); static bool isSameEvent(const Event &lhs, const Event &rhs); static bool isSameRentalCar(const RentalCar &lhs, const RentalCar &rhs); static bool isSameTaxiTrip(const Taxi &lhs, const Taxi &rhs); bool MergeUtil::isSame(const QVariant& lhs, const QVariant& rhs) { if (lhs.isNull() || rhs.isNull()) { return false; } if (lhs.userType() != rhs.userType()) { return false; } // for all reservations check underName if (JsonLd::canConvert(lhs)) { // for all: underName either matches or is not set const auto lhsUN = JsonLd::convert(lhs).underName().value(); const auto rhsUN = JsonLd::convert(rhs).underName().value(); if (!lhsUN.name().isEmpty() && !rhsUN.name().isEmpty() && !isSamePerson(lhsUN, rhsUN)) { return false; } } // flight: booking ref, flight number and departure day match if (JsonLd::isA(lhs)) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber() || lhsRes.reservationNumber().isEmpty()) { return false; } return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()); } if (JsonLd::isA(lhs)) { const auto lhsFlight = lhs.value(); const auto rhsFlight = rhs.value(); return isSameFlight(lhsFlight, rhsFlight); } // train: booking ref, train number and depature day match if (JsonLd::isA(lhs)) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) { return false; } return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()); } if (JsonLd::isA(lhs)) { const auto lhsTrip = lhs.value(); const auto rhsTrip = rhs.value(); return isSameTrainTrip(lhsTrip, rhsTrip); } // bus: booking ref, number and depature time match if (JsonLd::isA(lhs)) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) { return false; } return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()); } if (JsonLd::isA(lhs)) { const auto lhsTrip = lhs.value(); const auto rhsTrip = rhs.value(); return isSameBusTrip(lhsTrip, rhsTrip); } // hotel: booking ref, checkin day, name match if (JsonLd::isA(lhs)) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) { return false; } return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.checkinTime().date() == rhsRes.checkinTime().date(); } if (JsonLd::isA(lhs)) { const auto lhsHotel = lhs.value(); const auto rhsHotel = rhs.value(); return isSameLodingBusiness(lhsHotel, rhsHotel); } // Rental Car if (JsonLd::isA(lhs)) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) { return false; } return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.pickupTime().date() == rhsRes.pickupTime().date(); } if (JsonLd::isA(lhs)) { const auto lhsEv = lhs.value(); const auto rhsEv = rhs.value(); return isSameRentalCar(lhsEv, rhsEv); } // Taxi if (JsonLd::isA(lhs)) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) { return false; } return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.pickupTime().date() == rhsRes.pickupTime().date(); } if (JsonLd::isA(lhs)) { const auto lhsEv = lhs.value(); const auto rhsEv = rhs.value(); return isSameTaxiTrip(lhsEv, rhsEv); } // restaurant reservation: same restaurant, same booking ref, same day if (JsonLd::isA(lhs)) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) { return false; } auto endTime = rhsRes.endTime(); if (!endTime.isValid()) { endTime = QDateTime(rhsRes.startTime().date(), QTime(23, 59, 59)); } return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()) && lhsRes.startTime().date() == endTime.date(); } if (JsonLd::isA(lhs)) { const auto lhsRestaurant = lhs.value(); const auto rhsRestaurant = rhs.value(); return isSameFoodEstablishment(lhsRestaurant, rhsRestaurant); } // event reservation if (JsonLd::isA(lhs)) { const auto lhsRes = lhs.value(); const auto rhsRes = rhs.value(); if (lhsRes.reservationNumber() != rhsRes.reservationNumber()) { return false; } return isSame(lhsRes.reservationFor(), rhsRes.reservationFor()); } if (JsonLd::isA(lhs)) { const auto lhsEv = lhs.value(); const auto rhsEv = rhs.value(); return isSameEvent(lhsEv, rhsEv); } // tourist attraction visit if (JsonLd::isA(lhs)) { const auto l = lhs.value(); const auto r = rhs.value(); return isSameTouristAttractionVisit(l, r); } return true; } static bool isSameFlight(const Flight& lhs, const Flight& rhs) { // if there is a conflict on where this is going, or when, this is obviously not the same flight if (conflictIfPresent(lhs.departureAirport().iataCode(), rhs.departureAirport().iataCode()) || conflictIfPresent(lhs.arrivalAirport().iataCode(), rhs.arrivalAirport().iataCode()) || !equalAndPresent(lhs.departureDay(), rhs.departureDay())) { return false; } // same flight number and airline (on the same day) -> we assume same flight if (equalAndPresent(lhs.flightNumber(), rhs.flightNumber()) && equalAndPresent(lhs.airline().iataCode(), rhs.airline().iataCode())) { return true; } // we get here if we have matching origin/destination on the same day, but mismatching flight numbers // so this might be a codeshare flight // our caller checks for matching booking ref, so just look for a few counter-indicators here // (that is, if this is ever made available as standalone API, the last return should not be true) if (conflictIfPresent(lhs.departureTime(), rhs.departureTime())) { return false; } return true; } static bool isSameTrainTrip(const TrainTrip &lhs, const TrainTrip &rhs) { if (lhs.trainNumber().isEmpty() || rhs.trainNumber().isEmpty()) { return false; } return lhs.trainName() == rhs.trainName() && lhs.trainNumber() == rhs.trainNumber() && lhs.departureTime().date() == rhs.departureTime().date(); } static bool isSameBusTrip(const BusTrip &lhs, const BusTrip &rhs) { if (lhs.busNumber().isEmpty() || rhs.busNumber().isEmpty()) { return false; } return lhs.busName() == rhs.busName() && lhs.busNumber() == rhs.busNumber() && lhs.departureTime() == rhs.departureTime(); } static bool isSameLodingBusiness(const LodgingBusiness &lhs, const LodgingBusiness &rhs) { if (lhs.name().isEmpty() || rhs.name().isEmpty()) { return false; } return lhs.name() == rhs.name(); } static bool isSameFoodEstablishment(const FoodEstablishment &lhs, const FoodEstablishment &rhs) { if (lhs.name().isEmpty() || rhs.name().isEmpty()) { return false; } return lhs.name() == rhs.name(); } static bool isSameTouristAttractionVisit(const TouristAttractionVisit &lhs, const TouristAttractionVisit &rhs) { return lhs.arrivalTime() == rhs.arrivalTime() && isSameTouristAttraction(lhs.touristAttraction(), rhs.touristAttraction()); } static bool isSameTouristAttraction(const TouristAttraction &lhs, const TouristAttraction &rhs) { return lhs.name() == rhs.name(); } // compute the "difference" between @p lhs and @p rhs static QString diffString(const QString &lhs, const QString &rhs) { QString diff; // this is just a basic linear-time heuristic, this would need to be more something like // the Levenstein Distance algorithm for (int i = 0, j = 0; i < lhs.size() || j < rhs.size();) { - if (i < lhs.size() && j < rhs.size() && lhs[i].toCaseFolded() == rhs[j].toCaseFolded()) { + if (i < lhs.size() && j < rhs.size() && StringUtil::normalize(lhs[i]) == StringUtil::normalize(rhs[j])) { ++i; ++j; continue; } if ((j < rhs.size() && (lhs.size() < rhs.size() || (lhs.size() == rhs.size() && j < i))) || i == lhs.size()) { diff += rhs[j]; ++j; } else { diff += lhs[i]; ++i; } } return diff.trimmed(); } static bool isNameEqualish(const QString &lhs, const QString &rhs) { if (lhs.isEmpty() || rhs.isEmpty()) { return false; } auto diff = diffString(lhs, rhs).toUpper(); // remove honoric prefixes from the diff, in case the previous check didn't catch that diff.remove(QLatin1String("MRS")); diff.remove(QLatin1String("MR")); diff.remove(QLatin1String("MS")); // if there's letters in the diff, we assume this is different for (const auto c : diff) { if (c.isLetter()) { return false; } } return true; } bool MergeUtil::isSamePerson(const Person& lhs, const Person& rhs) { return isNameEqualish(lhs.name(), rhs.name()) || (isNameEqualish(lhs.givenName(), rhs.givenName()) && isNameEqualish(lhs.familyName(), rhs.familyName())); } static bool isSameEvent(const Event &lhs, const Event &rhs) { return equalAndPresent(lhs.name(), rhs.name()) && equalAndPresent(lhs.startDate(), rhs.startDate()); } static bool isSameRentalCar(const RentalCar &lhs, const RentalCar &rhs) { return lhs.name() == rhs.name(); } static bool isSameTaxiTrip(const Taxi &lhs, const Taxi &rhs) { //TODO verify return lhs.name() == rhs.name(); } static Airline mergeValue(const Airline &lhs, const Airline &rhs) { auto a = JsonLdDocument::apply(lhs, rhs).value(); // prefer the more detailed name if (a.name().size() < lhs.name().size()) { a.setName(lhs.name()); } return a; } static QDateTime mergeValue(const QDateTime &lhs, const QDateTime &rhs) { // prefer value with timezone return lhs.isValid() && lhs.timeSpec() == Qt::TimeZone && rhs.timeSpec() != Qt::TimeZone ? lhs : rhs; } QVariant MergeUtil::merge(const QVariant &lhs, const QVariant &rhs) { if (rhs.isNull()) { return lhs; } if (lhs.isNull()) { return rhs; } if (lhs.userType() != rhs.userType()) { qCWarning(Log) << "type mismatch during merging:" << lhs << rhs; return {}; } auto res = lhs; const auto mo = QMetaType(res.userType()).metaObject(); for (int i = 0; i < mo->propertyCount(); ++i) { const auto prop = mo->property(i); if (!prop.isStored()) { continue; } auto lv = prop.readOnGadget(lhs.constData()); auto rv = prop.readOnGadget(rhs.constData()); auto mt = rv.userType(); if (mt == qMetaTypeId()) { rv = mergeValue(lv.value(), rv.value()); } else if (mt == qMetaTypeId()) { rv = mergeValue(lv.toDateTime(), rv.toDateTime()); } else if (QMetaType(mt).metaObject()) { rv = merge(prop.readOnGadget(lhs.constData()), rv); } if (!rv.isNull()) { prop.writeOnGadget(res.data(), rv); } } return res; } diff --git a/src/stringutil.cpp b/src/stringutil.cpp index 2ed348e..bbbdd26 100644 --- a/src/stringutil.cpp +++ b/src/stringutil.cpp @@ -1,42 +1,49 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "stringutil.h" #include #include using namespace KItinerary; +QChar StringUtil::normalize(QChar c) +{ + // case folding + const auto n = c.toCaseFolded(); + + // if the character has a canonical decomposition use that and skip the + // combining diacritic markers following it + // see https://en.wikipedia.org/wiki/Unicode_equivalence + // see https://en.wikipedia.org/wiki/Combining_character + if (n.decompositionTag() == QChar::Canonical) { + return n.decomposition().at(0); + } + + return n; +} + QString StringUtil::normalize(const QString &str) { QString out; out.reserve(str.size()); - for (const auto chr : str) { - // case folding - auto c = chr.toCaseFolded(); - - // if the character has a canonical decomposition use that and skip the - // combining diacritic markers following it - if (c.decompositionTag() == QChar::Canonical) { - out.push_back(c.decomposition().at(0)); - } else { - out.push_back(c); - } + for (const auto c : str) { + out.push_back(normalize(c)); } return out; } diff --git a/src/stringutil.h b/src/stringutil.h index 1926c87..1ee57b2 100644 --- a/src/stringutil.h +++ b/src/stringutil.h @@ -1,38 +1,42 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KITINERARY_STRINGUTIL_H #define KITINERARY_STRINGUTIL_H #include "kitinerary_export.h" +class QChar; class QString; namespace KItinerary { /** String normalization and comparison utilities. */ namespace StringUtil { + /** Convert @p c to case-folded form and remove diacritic marks. */ + QChar normalize(QChar c); + /** Strips out diacritics and converts to case-folded form. * @internal only exported for unit tests */ KITINERARY_EXPORT QString normalize(const QString &str); } } #endif // KITINERARY_STRINGUTIL_H