Chris@0: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@0: Chris@0: #include "Objects.h" Chris@0: Chris@52: //#include Chris@0: Chris@0: #include Chris@0: #include Chris@0: Chris@11: #include "EditDistance.h" Chris@11: Chris@4: #include // to ensure correct qHash(const QString &) is found Chris@33: #include Chris@33: #include Chris@33: #include Chris@4: Chris@0: namespace ClassicalData { Chris@0: Chris@0: QMap Form::m_map; Chris@0: QMutex Form::m_mutex; Chris@0: Chris@37: QString Chris@37: Composition::getComposerName() const Chris@37: { Chris@37: if (m_composer) return m_composer->name(); Chris@37: return m_cname; Chris@37: } Chris@37: Chris@1: bool Chris@10: Composer::matchDates(const Composer *b) const Chris@1: { Chris@1: const Composer *a = this; Chris@1: Chris@1: if (a->birth() && b->birth()) { Chris@1: int ay = a->birth()->year(), by = b->birth()->year(); Chris@1: if (ay < 1800 || // birth dates before 1700 tend to be vague! Chris@1: a->birth()->approximate() || Chris@1: b->birth()->approximate()) { Chris@1: if (abs(ay - by) > 25) return false; Chris@1: } else { Chris@1: if (abs(ay - by) > 1) { Chris@1: return false; Chris@1: } Chris@1: } Chris@1: } Chris@1: if (a->death() && b->death()) { Chris@1: int ay = a->death()->year(), by = b->death()->year(); Chris@1: if (a->death()->approximate() || b->death()->approximate()) { Chris@1: if (abs(ay - by) > 10) return false; Chris@1: } else if (ay < 1700) { Chris@1: if (abs(ay - by) > 25) return false; Chris@1: } else if (ay < 1800) { Chris@1: // cut a bit of slack, but not as much as for birth date Chris@1: if (abs(ay - by) > 10) return false; Chris@1: } else { Chris@1: if (abs(ay - by) > 1) return false; Chris@1: } Chris@1: } Chris@1: return true; Chris@1: } Chris@1: Chris@11: void Chris@11: Composer::cacheNames() const Chris@11: { Chris@11: if (m_namesCached) return; Chris@11: Chris@11: QString n = name(); Chris@11: QStringList pl = n.split(", "); Chris@11: Chris@11: if (pl.size() == 1) { Chris@11: QStringList pl2; Chris@11: pl = n.split(' '); Chris@11: pl2.push_back(pl[pl.size()-1]); Chris@11: pl2.push_back(""); Chris@11: for (int i = 0; i+1 < pl.size(); ++i) { Chris@11: if (i > 0) pl2[1] += " "; Chris@11: pl2[1] += pl[i]; Chris@11: } Chris@11: pl = pl2; Chris@11: } Chris@11: Chris@11: m_surname = pl[0]; Chris@11: Chris@11: n = ""; Chris@11: for (int i = 1; i < pl.size(); ++i) { Chris@11: if (i > 1) n += ", "; Chris@11: n += pl[i]; Chris@11: } Chris@11: Chris@11: m_forenames = n; Chris@11: Chris@11: m_surnameElements.clear(); Chris@11: m_connectiveElements.clear(); Chris@11: m_forenameElements.clear(); Chris@11: m_otherElements.clear(); Chris@11: m_reducedSurnameElements.clear(); Chris@11: m_reducedForenameElements.clear(); Chris@11: Chris@13: static QRegExp sre("[\\., -]+"); Chris@13: Chris@13: foreach (QString s, m_surname.split(sre, QString::SkipEmptyParts)) { Chris@11: if (s[0].isUpper()) { Chris@11: m_surnameElements.push_back(s.toLower()); Chris@11: m_reducedSurnameElements.push_back(reduceName(s)); Chris@12: } else if (s.length() > 1) { Chris@11: m_connectiveElements.push_back(s.toLower()); Chris@11: } Chris@11: } Chris@11: Chris@13: foreach (QString s, m_forenames.split(sre, QString::SkipEmptyParts)) { Chris@11: if (s[0].isUpper()) { Chris@11: m_forenameElements.push_back(s.toLower()); Chris@11: m_reducedForenameElements.push_back(reduceName(s)); Chris@12: } else if (s.length() > 1) { Chris@11: m_connectiveElements.push_back(s.toLower()); Chris@11: } Chris@11: } Chris@11: Chris@11: foreach (QString a, m_aliases) { Chris@13: foreach (QString ae, a.split(sre, QString::SkipEmptyParts)) { Chris@11: m_otherElements.push_back(ae.toLower()); Chris@11: } Chris@11: } Chris@13: Chris@13: m_namesCached = true; Chris@11: } Chris@11: Chris@0: QString Chris@0: Composer::getSortName(bool caps) const Chris@0: { Chris@10: QString surname = getSurname(); Chris@10: QString forenames = getForenames(); Chris@10: if (caps) surname = surname.toUpper(); Chris@10: if (forenames != "") return surname + ", " + forenames; Chris@10: else return surname; Chris@10: } Chris@10: Chris@10: QString Chris@10: Composer::getSurname() const Chris@10: { Chris@11: cacheNames(); Chris@11: return m_surname; Chris@10: } Chris@10: Chris@10: QString Chris@10: Composer::getForenames() const Chris@10: { Chris@11: cacheNames(); Chris@11: return m_forenames; Chris@0: } Chris@0: Chris@0: QString Chris@0: Composer::getDisplayDates() const Chris@0: { Chris@0: QString s; Chris@0: if (birth() || death()) { Chris@0: bool showApprox = false; Chris@0: if ((birth() && birth()->approximate()) || Chris@0: (death() && death()->approximate())) { Chris@0: showApprox = true; Chris@0: } Chris@0: if (birth()) { Chris@0: if (birth()->place() != "") { Chris@0: s += birth()->place() + ", "; Chris@0: } Chris@0: if (showApprox) { Chris@0: s += "c. "; Chris@0: showApprox = false; Chris@0: } Chris@22: s += QString("%1").arg(birth()->year().toInt()); Chris@0: } Chris@0: s += "-"; Chris@0: if (death()) { Chris@0: if (death()->place() != "") { Chris@0: s += death()->place() + ", "; Chris@0: } Chris@0: if (showApprox) { Chris@0: s += "c. "; Chris@0: showApprox = false; Chris@0: } Chris@22: s += QString("%1").arg(death()->year().toInt()); Chris@0: } Chris@0: } Chris@0: Chris@0: return s; Chris@0: } Chris@10: Chris@10: static QString Chris@10: asciify(QString field) Chris@10: { Chris@10: QString ascii; Chris@10: for (int i = 0; i < field.length(); ++i) { Chris@10: QString dc = field[i].decomposition(); Chris@10: if (dc != "") ascii += dc[0]; Chris@10: else if (field[i] == QChar(0x00DF)) { Chris@10: ascii += "ss"; Chris@10: } else { Chris@10: ascii += field[i]; Chris@10: } Chris@10: } Chris@10: ascii.replace(QString::fromUtf8("\342\200\231"), "'"); // apostrophe Chris@10: ascii.replace(QString::fromUtf8("\342\200\222"), "-"); Chris@10: ascii.replace(QString::fromUtf8("\342\200\223"), "-"); Chris@10: ascii.replace(QString::fromUtf8("\342\200\224"), "-"); Chris@10: ascii.replace(QString::fromUtf8("\342\200\225"), "-"); Chris@10: return ascii; Chris@10: } Chris@10: Chris@10: QString Chris@10: Composer::reduceName(QString name) Chris@10: { Chris@10: QString key = asciify(name).toLower() Chris@10: .replace("'", "") Chris@10: .replace("x", "ks") Chris@10: .replace("y", "i") Chris@36: .replace("ie", "i") Chris@36: .replace("ei", "i") Chris@36: .replace("ii", "i") Chris@10: .replace("k", "c") Chris@10: .replace("aa", "a") Chris@36: .replace("a", "e") Chris@36: .replace("ee", "e") Chris@10: .replace("v", "f") Chris@36: .replace("ph", "f") Chris@10: .replace("ff", "f") Chris@10: .replace("th", "t") Chris@10: .replace("tch", "ch") Chris@36: .replace("ch", "c") Chris@36: .replace("cc", "c") Chris@10: .replace("er", "r"); Chris@10: return key; Chris@10: } Chris@10: Chris@10: bool Chris@10: Composer::matchCatalogueName(QString an) const Chris@10: { Chris@10: // ew! Chris@10: Chris@10: QString bn = name(); Chris@10: if (bn == an) return true; Chris@10: if (aliases().contains(an)) return true; Chris@10: Chris@10: int aSurnameIndex = 0, bSurnameIndex = 0; Chris@10: if (an.contains(",")) { Chris@10: an.replace(",", ""); Chris@10: } else { Chris@10: aSurnameIndex = -1; Chris@10: } Chris@10: if (bn.contains(",")) { Chris@10: bn.replace(",", ""); Chris@10: } else { Chris@10: bSurnameIndex = -1; Chris@10: } Chris@10: QStringList nl = an.split(QRegExp("[ -]")); Chris@10: QStringList bnl = reduceName(bn).split(QRegExp("[ -]")); Chris@10: int matchCount = 0; Chris@10: QString surnameMatch = ""; Chris@10: if (aSurnameIndex == -1) aSurnameIndex = nl.size()-1; Chris@10: if (bSurnameIndex == -1) bSurnameIndex = bnl.size()-1; Chris@10: if (nl[aSurnameIndex][0].isUpper() && Chris@10: nl[aSurnameIndex] != "Della" && Chris@10: reduceName(nl[aSurnameIndex]) == bnl[bSurnameIndex]) { Chris@10: surnameMatch = nl[aSurnameIndex]; Chris@10: } Chris@10: int tested = 0; Chris@10: foreach (QString elt, nl) { Chris@10: if (!elt[0].isUpper() || elt == "Della") continue; Chris@10: QString k = reduceName(elt); Chris@10: if (bnl.contains(k)) { Chris@10: ++matchCount; Chris@10: } Chris@10: if (++tested == 2 && matchCount == 0) { Chris@10: return false; Chris@10: } Chris@10: } Chris@10: if (surnameMatch != "") { Chris@52: // DEBUG << "namesFuzzyMatch: note: surnameMatch = " << surnameMatch << endl; Chris@10: if (matchCount > 1) { Chris@10: return true; Chris@10: } else { Chris@52: // DEBUG << "(but not enough else matched)" << endl; Chris@10: return false; Chris@10: } Chris@10: } Chris@10: return false; Chris@10: } Chris@10: Chris@14: float Chris@10: Composer::matchFuzzyName(QString n) const Chris@10: { Chris@13: int fameBonus = m_pages.size(); Chris@13: if (n == name()) return 100 + fameBonus; Chris@13: static QRegExp sre("[\\., -]+"); Chris@13: return matchFuzzyName(n.toLower().split(sre, QString::SkipEmptyParts)); Chris@13: } Chris@13: Chris@15: static int Chris@15: calculateThresholdedDistance(EditDistance &ed, const QString &user, Chris@15: const QString &machine) Chris@15: { Chris@15: int threshold = machine.length()/3; Chris@15: int dist; Chris@15: if (threshold == 0) dist = (user == machine ? 0 : -1); Chris@15: else { Chris@15: dist = ed.calculate(user, machine, threshold); Chris@15: if (dist > threshold) dist = -1; Chris@15: } Chris@15: return dist; Chris@15: } Chris@15: Chris@14: float Chris@13: Composer::matchFuzzyName(QStringList elements) const Chris@13: { Chris@14: if (elements.empty()) return 0; Chris@14: Chris@11: cacheNames(); Chris@11: int fameBonus = m_pages.size(); Chris@10: Chris@28: EditDistance ed(EditDistance::RestrictedTransposition); Chris@10: Chris@10: int score = 0; Chris@15: bool haveSurname = false; Chris@15: Chris@15: // We aim to scale the eventual result such that a score of 1.0 or Chris@15: // more indicates near-certainty that this is a correct match Chris@15: // (i.e. that it is properly matched -- not that it is the only Chris@15: // possible match). To achieve this score, we need to have Chris@15: // matched with reasonable confidence every element in the passed Chris@15: // elements list, and to have matched at least one of them to a Chris@15: // part of our surname. Chris@15: Chris@15: int matched = 0; Chris@15: int unmatched = 0; Chris@10: Chris@11: foreach (QString elt, elements) { Chris@10: Chris@11: bool accept = false; Chris@11: Chris@11: if (elt.length() == 1) { Chris@15: // An initial: search forenames only, ignoring Chris@15: // connectives. The score contribution here is low, but Chris@15: // they do not count to matched which means the score can Chris@15: // only enhance whatever happens elsewhere. They can Chris@15: // however seriously damage our score if unmatched, which Chris@15: // is as it should be. Chris@11: foreach (QString s, m_forenameElements) { Chris@11: if (s[0] == elt[0]) { Chris@15: score += 2; Chris@11: accept = true; Chris@10: break; Chris@10: } Chris@10: } Chris@11: if (!accept) { Chris@15: foreach (QString s, m_connectiveElements) { Chris@15: if (s[0] == elt[0]) { Chris@15: score += 1; Chris@15: accept = true; Chris@15: break; Chris@15: } Chris@15: } Chris@10: } Chris@15: if (!accept) { Chris@15: foreach (QString s, m_surnameElements) { Chris@15: if (s[0] == elt[0]) { Chris@15: // no score, but don't call it unmatched Chris@15: accept = true; Chris@15: break; Chris@15: } Chris@15: } Chris@15: } Chris@15: if (!accept) ++unmatched; Chris@10: continue; Chris@10: } Chris@11: Chris@11: foreach (QString s, m_surnameElements) { Chris@15: int dist = calculateThresholdedDistance(ed, elt, s); Chris@15: if (dist >= 0) { Chris@15: score += 22 - dist*2; Chris@15: if (elt[0] != s[0]) score -= 10; Chris@15: accept = true; Chris@13: // std::cerr << "[surname: " << s.toStdString() << "]" << std::endl; Chris@10: break; Chris@10: } Chris@10: } Chris@15: if (accept) { Chris@15: haveSurname = true; Chris@15: ++matched; Chris@15: continue; Chris@15: } Chris@10: Chris@11: foreach (QString s, m_forenameElements) { Chris@15: int dist = calculateThresholdedDistance(ed, elt, s); Chris@15: if (dist >= 0) { Chris@15: score += 22 - dist*2; Chris@15: if (elt[0] != s[0]) score -= 10; Chris@15: accept = true; Chris@13: // std::cerr << "[forename: " << s.toStdString() << "]" << std::endl; Chris@10: break; Chris@10: } Chris@10: } Chris@15: if (accept) { Chris@15: ++matched; Chris@15: continue; Chris@15: } Chris@10: Chris@11: foreach (QString s, m_connectiveElements) { Chris@15: // treated much like initials Chris@15: int dist = calculateThresholdedDistance(ed, elt, s); Chris@15: if (dist == 0) { Chris@15: score += 2; Chris@15: accept = true; Chris@15: } else if (dist == 1) { Chris@15: score += 1; Chris@15: accept = true; Chris@15: } Chris@11: if (accept) { Chris@13: // std::cerr << "[connective: " << s.toStdString() << "]" << std::endl; Chris@10: break; Chris@10: } Chris@10: } Chris@15: if (accept) { Chris@11: continue; Chris@11: } Chris@11: Chris@15: QString reduced = reduceName(elt); Chris@15: Chris@16: //!!! these don't seem to match often... Chris@16: Chris@15: if (m_reducedSurnameElements.contains(reduced)) { Chris@15: score += 10; Chris@15: haveSurname = true; Chris@15: ++matched; Chris@15: std::cerr << "[reduced surname: " << elt.toStdString() << "]" << std::endl; Chris@15: continue; Chris@15: } Chris@15: Chris@15: if (m_reducedForenameElements.contains(reduced)) { Chris@11: score += 7; Chris@15: ++matched; Chris@15: std::cerr << "[reduced forename: " << elt.toStdString() << "]" << std::endl; Chris@11: continue; Chris@11: } Chris@11: Chris@11: foreach (QString s, m_otherElements) { Chris@15: int dist = calculateThresholdedDistance(ed, elt, s); Chris@15: if (dist >= 0) { Chris@15: score += 22 - dist*2; Chris@15: if (elt[0] != s[0]) score -= 10; Chris@15: accept = true; Chris@13: // std::cerr << "[other: " << s.toStdString() << "]" << std::endl; Chris@10: break; Chris@10: } Chris@10: } Chris@15: if (accept) { Chris@15: ++matched; Chris@15: continue; Chris@15: } Chris@10: Chris@15: ++unmatched; Chris@11: } Chris@15: Chris@15: // if (fameBonus > 0) std::cerr << "[fame: " << fameBonus << "]" << std::endl; Chris@15: score += fameBonus; Chris@10: Chris@15: if (matched == 0) { Chris@15: if (unmatched == 0) { Chris@15: return float(score) / 20.f; Chris@15: } else { Chris@15: return 0; Chris@15: } Chris@11: } Chris@15: Chris@15: float fscore = score; Chris@15: float divisor = (matched + unmatched) * 20; Chris@15: Chris@15: if (!haveSurname) fscore /= 2; Chris@15: if (unmatched > 0) fscore /= 1.5; Chris@15: Chris@15: fscore /= divisor; Chris@15: Chris@15: if (matched > 0) { Chris@15: // std::cerr << "[score " << score << " with divisor " << divisor << " for " << name().toStdString() << " adjusted to " << fscore << "]" << std::endl; Chris@15: } Chris@15: Chris@15: return fscore; Chris@10: } Chris@0: Chris@16: float Chris@19: Composer::matchTyping(QString t) const Chris@16: { Chris@28: return doMatchTyping(t, false); Chris@28: } Chris@28: Chris@28: float Chris@28: Composer::matchTypingQuick(QString t) const Chris@28: { Chris@28: return doMatchTyping(t, true); Chris@28: } Chris@28: Chris@28: float Chris@28: Composer::doMatchTyping(QString t, bool quick) const Chris@28: { Chris@19: if (t == "") return 0; Chris@16: Chris@16: cacheNames(); Chris@28: float fameBonus = m_pages.size() / 400.f; Chris@16: Chris@28: QString n = name().toLower(); Chris@28: t = t.toLower(); Chris@16: Chris@19: if (n == t) return 1.f + fameBonus; Chris@19: if (n.startsWith(t)) return 0.8f + fameBonus; Chris@28: Chris@28: QSet sl; Chris@28: QSet nl; Chris@28: foreach (QString s, m_surnameElements) { Chris@28: sl.insert(s.toLower()); Chris@28: nl.insert(s.toLower()); Chris@28: } Chris@28: foreach (QString s, m_forenameElements) { Chris@28: nl.insert(s.toLower()); Chris@28: } Chris@28: if (!quick) { Chris@28: foreach (QString s, m_otherElements) { Chris@28: nl.insert(s.toLower()); Chris@28: } Chris@28: foreach (QString s, m_connectiveElements) { Chris@28: nl.insert(s.toLower()); Chris@28: } Chris@28: } Chris@28: Chris@28: static QRegExp sre("[\\., -]+"); Chris@28: QStringList tl = t.split(sre, QString::SkipEmptyParts); Chris@19: Chris@16: float score = 0.f; Chris@16: Chris@19: if (nl.empty() || tl.empty()) return 0.f; Chris@19: Chris@19: int unmatched = 0; Chris@28: Chris@19: for (int i = 0; i < tl.size(); ++i) { Chris@28: Chris@28: QString tel = tl[i]; Chris@28: float component = 0.f; Chris@28: float max = 0.f; Chris@28: Chris@28: for (QSet::const_iterator ni = nl.begin(); Chris@28: ni != nl.end(); ++ni) { Chris@28: Chris@28: QString nel = ni->toLower(); Chris@28: Chris@28: if (tel == nel) { Chris@28: if (tel.length() > 1) { Chris@28: component = 0.2; Chris@19: } else { Chris@28: component = 0.1; Chris@19: } Chris@28: if (sl.contains(nel)) component *= 1.5; Chris@28: goto calculated; Chris@19: } Chris@28: Chris@28: if (nel.startsWith(tel)) { Chris@28: component = 0.1; Chris@28: if (sl.contains(nel)) component *= 1.5; Chris@28: goto calculated; Chris@28: } Chris@28: Chris@28: if (!quick) { Chris@29: if (tel.length() > 3) { Chris@28: EditDistance ed(EditDistance::RestrictedTransposition); Chris@28: int dist = calculateThresholdedDistance Chris@29: (ed, nel.left(tel.length()), tel); Chris@28: if (dist >= 0) { Chris@28: component = 0.08 - dist * 0.01; Chris@28: if (sl.contains(nel)) component *= 1.5; Chris@28: } Chris@28: } Chris@28: if (component > 0.f) goto calculated; Chris@28: } Chris@28: Chris@28: if (nel.startsWith(tel[0])) { Chris@28: component += 0.02; Chris@28: } Chris@28: Chris@28: calculated: Chris@28: if (component > max) max = component; Chris@16: } Chris@28: Chris@28: score += max; Chris@16: } Chris@16: Chris@28: if (!quick) { Chris@28: if (t.contains(" ")) { Chris@28: float fuzzyScore = matchFuzzyName(t); Chris@28: if (fuzzyScore >= 0.4f) { Chris@28: score += fuzzyScore / 3.f; Chris@28: } Chris@19: } Chris@19: } Chris@19: Chris@16: if (score > 0.f) score += fameBonus; Chris@16: return score; Chris@16: } Chris@16: Chris@24: void Chris@24: Composer::mergeFrom(Composer *c) Chris@24: { Chris@24: QSet allNames = c->aliases(); Chris@25: allNames.insert(c->name()); Chris@24: Chris@24: foreach (QString n, allNames) { Chris@24: if (n != m_name && !m_aliases.contains(n)) { Chris@24: m_aliases.insert(n); Chris@24: m_namesCached = false; Chris@24: } Chris@24: } Chris@24: Chris@24: if (!m_birth) { Chris@31: if (c->birth()) { Chris@31: m_birth = new Birth(*c->birth()); Chris@31: emit birthChanged(m_birth); Chris@31: } Chris@24: } Chris@24: Chris@24: if (!m_death) { Chris@31: if (c->death()) { Chris@31: m_death = new Death(*c->death()); Chris@31: emit deathChanged(m_death); Chris@31: } Chris@24: } Chris@24: Chris@24: if (c->gender() != "") { Chris@24: if (m_gender == "") { Chris@24: m_gender = c->gender(); Chris@31: emit genderChanged(m_gender); Chris@24: } else if (c->gender() != m_gender) { Chris@24: std::cerr << "WARNING: Composer::mergeFrom: Gender mismatch! Composer " << c->name().toStdString() << " has gender " << c->gender().toStdString() << ", but target composer " << m_name.toStdString() << " has gender " << m_gender.toStdString() << std::endl; Chris@24: } Chris@24: } Chris@24: Chris@24: m_nationality.unite(c->nationality()); Chris@24: m_geonameURIs.unite(c->geonameURIs()); Chris@24: m_otherURIs.unite(c->otherURIs()); Chris@26: Chris@26: foreach (Document *d, c->pages()) { Chris@38: /* Chris@26: Document *dd = new Document; Chris@26: dd->setUri(d->uri()); Chris@38: dd->setSiteName(d->siteName()); Chris@26: dd->setTopic(this); Chris@26: m_pages.insert(dd); Chris@38: */ Chris@38: d->setTopic(this); Chris@38: m_pages.insert(d); Chris@26: } Chris@24: Chris@24: if (m_period == "") m_period = c->period(); Chris@24: if (m_remarks == "") m_remarks = c->remarks(); Chris@31: Chris@31: emit nationalityChanged(m_nationality); Chris@31: emit geonameURIsChanged(m_geonameURIs); Chris@31: emit otherURIsChanged(m_otherURIs); Chris@31: emit pagesChanged(m_pages); Chris@31: emit periodChanged(m_period); Chris@31: emit remarksChanged(m_remarks); Chris@31: emit aliasesChanged(m_aliases); Chris@24: } Chris@24: Chris@37: QString Chris@37: Work::getComposerName() const Chris@37: { Chris@37: Composer *c = getComposer(); Chris@37: if (c) return c->name(); Chris@37: else return ""; Chris@37: } Chris@37: Chris@0: static int Chris@0: compare(QString a, QString b) Chris@0: { Chris@0: if (a < b) { Chris@0: return -1; Chris@0: } else if (a > b) { Chris@0: return 1; Chris@0: } else { Chris@0: return 0; Chris@0: } Chris@0: } Chris@0: Chris@10: int Chris@10: Work::compareCatalogueNumberTexts(QString a, QString b) Chris@0: { Chris@0: // std::cout << "compare " << a.toStdString() Chris@34: // << " :: " << b.toStdString() << std::endl; Chris@0: Chris@0: if (a == b) return 0; Chris@0: Chris@0: if (!a[0].isDigit()) { Chris@34: a.replace(QRegExp("^[^\\d]+"), ""); Chris@34: } Chris@34: Chris@34: if (!b[0].isDigit()) { Chris@34: b.replace(QRegExp("^[^\\d]+"), ""); Chris@34: } Chris@34: Chris@34: QStringList al = a.split(QRegExp("\\b[^\\d]*"), QString::SkipEmptyParts); Chris@34: QStringList bl = b.split(QRegExp("\\b[^\\d]*"), QString::SkipEmptyParts); Chris@34: if (al.size() != bl.size()) return int(al.size()) - int(bl.size()); Chris@34: Chris@34: /* if (al.size() < 2 || bl.size() < 2 || al.size() != bl.size()) { Chris@34: if (a < b) return -1; Chris@34: else if (a > b) return 1; Chris@34: else return 0; Chris@34: } Chris@34: */ Chris@34: for (int i = 0; i < al.size(); ++i) { Chris@34: if (al[i] != bl[i]) { Chris@34: // use atoi instead of toInt() because we want it to succeed even Chris@34: // if the text is not only an integer (e.g. 35a) Chris@34: int aoi = atoi(al[i].toLocal8Bit().data()); Chris@34: int boi = atoi(bl[i].toLocal8Bit().data()); Chris@34: if (aoi != boi) return aoi - boi; Chris@34: else return compare(al[i], bl[i]); Chris@0: } Chris@0: } Chris@34: return 0; Chris@34: } Chris@0: Chris@34: QStringList Chris@34: Work::extractCatalogueNumberTexts(QString text) Chris@34: { Chris@34: //!!! test this Chris@34: QStringList results; Chris@34: std::cerr << "Work::extractCatalogueNumberTexts(" << text.toStdString() << ")" << std::endl; Chris@0: Chris@34: // Note we explicitly exclude "catalogue identifiers" beginning Chris@34: // with N, because we don't want to treat e.g. "Symphony No. 8" Chris@34: // as catalogue number 8. What a fine hack. Chris@34: Chris@37: QRegExp catre("\\b([Oo]pu?s?|[A-MP-Z]+)\\.?[\\s_]*(\\d+\\w*)(\\s+[Nn]([OoRrBb]?|umber)(\\.\\s*|\\s+)(\\d+\\w*))?\\b"); Chris@34: int ix = 0; Chris@34: while ((ix = catre.indexIn(text, ix+1)) >= 0) { Chris@34: std::cerr << "extractCatalogueNumberTexts: found match \"" << catre.cap(0).toStdString() << "\"" << std::endl; Chris@37: QString cat = catre.cap(0); Chris@37: // ensure space before digit Chris@37: for (int i = 0; i+1 < cat.length(); ++i) { Chris@37: if (!cat[i].isDigit() && !cat[i].isSpace() && cat[i+1].isDigit()) { Chris@37: QString spaced = cat.left(i+1) + " " + cat.right(cat.length()-i-1); Chris@37: std::cerr << "spaced out from " << cat.toStdString() << " to " Chris@37: << spaced.toStdString() << std::endl; Chris@37: cat = spaced; Chris@37: break; Chris@37: } Chris@37: } Chris@37: results.push_back(cat); Chris@34: } Chris@34: return results; Chris@0: } Chris@0: Chris@0: bool Chris@0: Work::Ordering::operator()(Work *a, Work *b) Chris@0: { Chris@0: if (!a) { Chris@0: if (!b) return false; Chris@0: else return true; Chris@0: } else { Chris@0: if (!b) { Chris@0: return false; Chris@0: } Chris@0: } Chris@0: /* Chris@0: QString ao = a->catalogue(); Chris@0: if (ao == "") ao = a->opus(); Chris@0: Chris@0: QString bo = b->catalogue(); Chris@0: if (bo == "") bo = b->opus(); Chris@0: Chris@0: std::cout << "ao " << ao.toStdString() << ", bo " << bo.toStdString() << std::endl; Chris@0: */ Chris@0: int c = 0; Chris@0: if (a->catalogue() != "" && b->catalogue() != "") { Chris@10: c = compareCatalogueNumberTexts(a->catalogue(), b->catalogue()); Chris@0: } Chris@0: if (c == 0 && a->opus() != "" && b->opus() != "") { Chris@10: c = compareCatalogueNumberTexts(a->opus(), b->opus()); Chris@0: } Chris@0: if (c == 0 && a->partOf() == b->partOf() && Chris@0: a->number() != "" && b->number() != "") { Chris@10: c = compareCatalogueNumberTexts(a->number(), b->number()); Chris@0: } Chris@0: Chris@0: bool rv = false; Chris@0: Chris@0: if (c == 0) { Chris@0: if (a->name() == b->name()) rv = (a < b); Chris@0: else rv = (a->name() < b->name()); Chris@0: } else { Chris@0: rv = (c < 0); Chris@0: } Chris@0: Chris@0: // std::cout << "result = " << rv << std::endl; Chris@0: return rv; Chris@0: } Chris@0: Chris@37: QString Chris@37: Work::getDisplayName() const Chris@37: { Chris@37: QString suffix; Chris@37: Chris@37: if (catalogue() != "") { Chris@37: suffix = catalogue(); Chris@37: } else if (opus() != "") { Chris@37: suffix = QString("Op. %1").arg(opus()); Chris@37: } Chris@37: if (suffix != "" && number() != "") { Chris@37: suffix = QString("%1 no. %2").arg(suffix).arg(number()); Chris@37: } Chris@37: if (suffix != "") { Chris@37: if (name() != "") { Chris@37: return QString("%1, %2").arg(name()).arg(suffix); Chris@37: } else { Chris@37: return suffix; Chris@37: } Chris@37: } else { Chris@37: return name(); Chris@37: } Chris@37: } Chris@37: Chris@45: AudioFile::AudioFile(QObject *parent) : Chris@45: QObject(parent) Chris@43: { Chris@43: } Chris@43: Chris@45: AudioFile::AudioFile(FileSource source, QObject *parent) : Chris@45: QObject(parent) Chris@33: { Chris@33: if (source.isAvailable()) { Chris@33: QFile f(source.getLocalFilename()); Chris@33: f.open(QIODevice::ReadOnly); Chris@45: //!!! stream this! Chris@33: QByteArray ba = f.readAll(); Chris@52: m_hash = QString::fromLatin1 Chris@33: (QCryptographicHash::hash(ba, QCryptographicHash::Sha1).toHex()); Chris@33: } Chris@33: QString location = source.getLocation(); Chris@33: if (source.isRemote()) { Chris@33: m_uri = Dataquay::Uri(location); Chris@33: } else { Chris@33: if (location.contains("://")) { Chris@33: m_uri = Dataquay::Uri(location); Chris@33: } else if (location.startsWith('/')) { Chris@33: m_uri = Dataquay::Uri("file://" + location); Chris@33: } else { Chris@33: m_uri = Dataquay::Uri("file://" + QFileInfo(location).canonicalFilePath()); Chris@33: } Chris@33: } Chris@45: Chris@45: std::cerr << "AudioFile::AudioFile: hash = " << m_hash.toStdString() Chris@33: << ", uri = " << m_uri.toString().toStdString() << std::endl; Chris@33: } Chris@33: Chris@48: AudioFile::~AudioFile() Chris@48: { Chris@48: foreach (AudioFileTag *t, m_tags) delete t; Chris@48: } Chris@48: Chris@48: void Chris@48: AudioFile::setTags(QSet tt) Chris@48: { Chris@48: foreach (AudioFileTag *t, m_tags) { Chris@48: if (!tt.contains(t)) delete t; Chris@48: } Chris@48: m_tags = tt; Chris@48: } Chris@48: Chris@48: void Chris@48: AudioFile::addTag(AudioFileTag *t) Chris@48: { Chris@48: m_tags.insert(t); Chris@48: } Chris@0: Chris@0: } Chris@0: