/* Copyright 2006-2026 The QElectroTech Team This file is part of QElectroTech. QElectroTech is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. QElectroTech 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 General Public License for more details. You should have received a copy of the GNU General Public License along with QElectroTech. If not, see . */ #include "positionorder.h" #include #include #include /** PositionOrder -- the position comparisons behind renumbering elements (comparPos) and numbering terminals. std::sort needs a strict weak ordering; the old comparisons used "<=" and a 1 px tolerance, which are not one, so two elements at the same position (or three terminals a fraction apart) were undefined behaviour. */ class tst_positionorder : public QObject { Q_OBJECT using Less = std::function; static QList> orders() { return {{QStringLiteral("xThenY"), PositionOrder::xThenY}, {QStringLiteral("yThenX"), PositionOrder::yThenX}, {QStringLiteral("roundedXThenY"), PositionOrder::roundedXThenY}, {QStringLiteral("roundedYThenX"), PositionOrder::roundedYThenX}}; } private slots: // Nothing is before itself, and equal points are before each other // in neither direction: what "<=" broke. void equalPointsAreNotOrdered() { const QPointF p(100, 200), q(100, 200); for (const auto &order : orders()) { QVERIFY2(!order.second(p, p), qPrintable(order.first)); QVERIFY2(!order.second(p, q), qPrintable(order.first)); QVERIFY2(!order.second(q, p), qPrintable(order.first)); } } void leftToRightThenTopToBottom() { QVERIFY(PositionOrder::xThenY(QPointF(10, 500), QPointF(20, 0))); QVERIFY(!PositionOrder::xThenY(QPointF(20, 0), QPointF(10, 500))); QVERIFY(PositionOrder::xThenY(QPointF(10, 0), QPointF(10, 5))); QVERIFY(!PositionOrder::xThenY(QPointF(10, 5), QPointF(10, 0))); } void topToBottomThenLeftToRight() { QVERIFY(PositionOrder::yThenX(QPointF(500, 10), QPointF(0, 20))); QVERIFY(!PositionOrder::yThenX(QPointF(0, 20), QPointF(500, 10))); QVERIFY(PositionOrder::yThenX(QPointF(0, 10), QPointF(5, 10))); QVERIFY(!PositionOrder::yThenX(QPointF(5, 10), QPointF(0, 10))); } // Items a fraction of a pixel apart (placed with Ctrl, or after a // rotation) count as aligned, and are then ordered on the other axis. void roundedTreatsFractionsAsAligned() { QVERIFY(PositionOrder::roundedXThenY(QPointF(10.2, 0), QPointF(9.8, 5))); QVERIFY(!PositionOrder::roundedXThenY(QPointF(9.8, 5), QPointF(10.2, 0))); QVERIFY(PositionOrder::roundedYThenX(QPointF(0, 10.2), QPointF(5, 9.8))); QVERIFY(!PositionOrder::roundedYThenX(QPointF(5, 9.8), QPointF(0, 10.2))); } // The three terminals that broke the tolerance comparison: with // "within 1 px counts as aligned", a < b (by y), b < c (by y) and // c < a (by x), a cycle. Rounded, they are simply ordered by x. void roundedIsTransitiveWhereToleranceWasNot() { const QPointF a(2, 0), b(1.1, 1), c(0.2, 2); QVERIFY(PositionOrder::roundedXThenY(c, b)); QVERIFY(PositionOrder::roundedXThenY(b, a)); QVERIFY(PositionOrder::roundedXThenY(c, a)); QVERIFY(!PositionOrder::roundedXThenY(a, c)); } // Every order is a strict weak ordering on a grid of awkward points: // irreflexive, asymmetric, transitive, and with transitive // equivalence. Checked by brute force on the whole set. void strictWeakOrdering_data() { QTest::addColumn("name"); for (const auto &order : orders()) QTest::newRow(qPrintable(order.first)) << order.first; } void strictWeakOrdering() { QFETCH(QString, name); Less less; for (const auto &order : orders()) if (order.first == name) less = order.second; QVERIFY(less); QList points; const QList values{-1, -0.6, -0.4, 0, 0.2, 0.5, 0.9, 1, 1.1, 2, 10.5, 61.3, 61.7}; for (qreal x : values) for (qreal y : values) points << QPointF(x, y); points << points.first() << QPointF(2, 0) << QPointF(1.1, 1) << QPointF(0.2, 2); auto equiv = [&](const QPointF &p, const QPointF &q) { return !less(p, q) && !less(q, p); }; for (const QPointF &p : points) { QVERIFY(!less(p, p)); for (const QPointF &q : points) { if (less(p, q)) QVERIFY(!less(q, p)); for (const QPointF &r : points) { if (less(p, q) && less(q, r)) QVERIFY2(less(p, r), "transitive"); if (equiv(p, q) && equiv(q, r)) QVERIFY2(equiv(p, r), "equivalence transitive"); } } } // And std::sort on it ends sorted, with the list intact. QList sorted = points; std::sort(sorted.begin(), sorted.end(), less); QCOMPARE(sorted.size(), points.size()); for (int i = 1; i < sorted.size(); ++i) QVERIFY(!less(sorted.at(i), sorted.at(i - 1))); } // Twenty items at one position, as pasting at the origin leaves // them: a list std::sort could scan past the end of with "<=". void manyEqualPositionsSort() { QList points(20, QPointF(0, 0)); points << QPointF(-10, 0) << QPointF(10, 0); std::sort(points.begin(), points.end(), PositionOrder::xThenY); QCOMPARE(points.size(), 22); QCOMPARE(points.first(), QPointF(-10, 0)); QCOMPARE(points.last(), QPointF(10, 0)); } }; QTEST_GUILESS_MAIN(tst_positionorder) #include "tst_positionorder.moc"