Add a canonicalize function for domains

This commit is contained in:
Nicolas Évrard
2023-08-14 22:32:14 +02:00
parent 99211dd03d
commit 82ae036d3a
8 changed files with 288 additions and 3 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add a canonicalize function for domains
* Add menu to copy selected record from tree view to clipboard
* Support PYSON comparison of timedelta
* Support encoding timedelta into PYSON TimeDelta
+76
View File
@@ -2436,6 +2436,63 @@
e => this.localize_domain(e, field_name, strip_target));
}
},
_sort_key: function(domain) {
if (!domain.length) {
return [0, domain];
} else if (this.is_leaf(domain)) {
if (domain[2] === null) {
domain = domain.slice();
domain[2] = Number.NEGATIVE_INFINITY;
}
return [1, domain];
} else if (~['AND', 'OR'].indexOf(domain)) {
return [0, domain];
} else {
var content = domain.map(this._sort_key.bind(this));
var nestedness = Math.max(...content.map(e => e[0]));
return [nestedness + 1, content];
}
},
_domain_compare: function(d1, d2) {
if ((d1 instanceof Array) && (d2 instanceof Array)) {
var elem_comparison;
var min_len = Math.min(d1.length, d2.length);
for (var i = 0; i < min_len; i++) {
elem_comparison = this._domain_compare(d1[i], d2[i]);
if (elem_comparison != 0) {
return elem_comparison;
}
}
if (d1.length == d2.length) {
return 0;
} else {
return d1.length < d2.length ? -1 : 1;
}
} else if (d1 == d2) {
return 0;
} else {
return d1 < d2 ? -1 : 1;
}
},
sort: function(domain) {
if (!domain.length) {
return [];
} else if (this.is_leaf(domain)) {
return domain;
} else if (~['AND', 'OR'].indexOf(domain)) {
return domain;
} else {
var sorted_elements = domain.map(this.sort.bind(this));
sorted_elements.sort(
(d1, d2) => this._domain_compare(this._sort_key(d1), this._sort_key(d2))
);
return sorted_elements;
}
},
prepare_reference_domain: function(domain, reference) {
var value2reference = function(value) {
@@ -2649,6 +2706,25 @@
return this.simplify_nested(this.simplify_duplicate(domain));
}
},
simplify_AND: function(domain) {
if (this.is_leaf(domain)) {
return domain;
} else if (domain == 'OR') {
return domain;
} else {
var simplified = [];
for (const e of domain) {
if (e == 'AND') {
continue;
}
simplified.push(this.simplify_AND(e));
}
return simplified;
}
},
canonicalize: function(domain) {
return this.simplify_AND(this.sort(this.simplify(domain)));
},
merge: function(domain, domoperator) {
if (jQuery.isEmptyObject(domain) ||
~['AND', 'OR'].indexOf(domain)) {
+59
View File
@@ -3116,6 +3116,65 @@
[]));
});
QUnit.test('DomainInversion.sort', function() {
var domain_inversion = new Sao.common.DomainInversion();
var sort = domain_inversion.sort.bind(domain_inversion);
var tests = [
[[], []],
[['AND'], ['AND']],
[['OR'], ['OR']],
[[['foo', '=', 1]], [['foo', '=', 1]]],
[[['foo', '=', 1000], ['foo', '=', 0]],
[['foo', '=', 0], ['foo', '=', 1000]]],
[[['foo', '=', 1], ['foo', '=', null]],
[['foo', '=', null], ['foo', '=', 1]]],
[[['foo', '=', 1], []], [[], ['foo', '=', 1]]],
[[[[[['foo', '=', 1]]]], [[['foo', '=', 1]]]],
[[[['foo', '=', 1]]], [[[['foo', '=', 1]]]]]],
[[[['foo', '=', 1], ['bar', '=', 2]],
[['foo', '=', 1], ['bar', '=', 2], ['baz', '=', 3]]],
[[['bar', '=', 2], ['baz', '=', 3], ['foo', '=', 1]],
[['bar', '=', 2], ['foo', '=', 1]]]],
[['OR', ['foo', '=', 1], ['foo', '=', null],
['AND', ['bar', '=', 2], ['baz', '=', 3]]],
['OR', ['foo', '=', null], ['foo', '=', 1],
['AND', ['bar', '=', 2], ['baz', '=', 3]]]],
];
for (const [domain, expected] of tests) {
QUnit.deepEqual(expected, sort(domain));
}
});
QUnit.test('DomainInversion.canonicalize', function() {
var domain_inversion = new Sao.common.DomainInversion();
var canonicalize = domain_inversion.canonicalize.bind(domain_inversion);
var tests = [
[[], []],
[['AND'], []],
[['OR'], []],
[[['foo', '=', 1], ['bar', '=', 2]],
[['bar', '=', 2], ['foo', '=', 1]]],
[[[['foo', '=', 1]]], [['foo', '=', 1]]],
[['AND', ['OR', ['bar', '=', 2], ['baz', '=', 3]],
['foo', '=', 1]],
[['foo', '=', 1],
['OR', ['bar', '=', 2], ['baz', '=', 3]]]],
[['OR', ['AND', ['bar', '=', 2], ['baz', '=', 3]],
['foo', '=', 1]],
['OR', ['foo', '=', 1],
[['bar', '=', 2], ['baz', '=', 3]]]],
[['AND', ['AND', ['bar', '=', 2], ['baz', '=', 3]],
['foo', '=', 1]],
[['bar', '=', 2], ['baz', '=', 3], ['foo', '=', 1]]],
[['OR', ['OR', ['bar', '=', 2], ['baz', '=', 3]],
['foo', '=', 1]],
['OR', ['bar', '=', 2], ['baz', '=', 3], ['foo', '=', 1]]],
];
for (const [domain, expected] of tests) {
QUnit.deepEqual(expected, canonicalize(domain));
}
});
QUnit.test('DomainParser.completion', function() {
var compare = Sao.common.compare;
var parser = new Sao.common.DomainParser({
+1
View File
@@ -1,3 +1,4 @@
* Add a canonicalize function for domains
* Support PYSON comparison of timedelta
* Support encoding timedelta into PYSON TimeDelta
* Transfer configuration, profiles and plugins from previous version
+47
View File
@@ -284,6 +284,34 @@ def localize_domain(domain, field_name=None, strip_target=False):
for part in domain]
def _sort_key(domain):
if not domain:
return (0, tuple())
elif is_leaf(domain):
if domain[2] is None:
domain = list(domain).copy()
domain[2] = float('-inf')
return (1, tuple(domain))
elif domain in ['AND', 'OR']:
return (0, domain)
else:
content = tuple(_sort_key(e) for e in domain)
nestedness = max(k[0] for k in content)
return (nestedness + 1, content)
def sort(domain):
"Sort a domain"
if not domain:
return domain
elif is_leaf(domain):
return domain
elif domain in ['AND', 'OR']:
return domain
else:
return sorted((sort(e) for e in domain), key=_sort_key)
def bool_operator(domain):
"Returns the boolean operator used by a domain"
bool_op = 'AND'
@@ -340,6 +368,16 @@ def simplify_duplicate(domain):
return dedup_branches
def simplify_AND(domain):
"""Remove useless ANDs"""
if is_leaf(domain):
return domain
elif domain == 'OR':
return domain
else:
return [simplify_AND(e) for e in domain if e != 'AND']
def simplify(domain):
"""Remove duplicate expressions and useless OR/AND"""
if is_leaf(domain):
@@ -350,6 +388,15 @@ def simplify(domain):
return simplify_nested(simplify_duplicate(domain))
def canonicalize(domain):
"""Returns the canonical version of a domain.
The canonical version of a domain is one where the domain is both
simplified and sorted
"""
return simplify_AND(sort(simplify(domain)))
def merge(domain, domoperator=None):
if not domain or domain in ('AND', 'OR'):
return []
+1
View File
@@ -1,3 +1,4 @@
* Add a canonicalize function for domains
* Enforce record rules when reading only non SQL fields (#12428)
* Support PYSON comparison of timedelta
* Support encoding timedelta into PYSON TimeDelta
+56 -3
View File
@@ -17,9 +17,9 @@ from trytond.tools import (
reduce_ids, remove_forbidden_chars, rstrip_wildcard, slugify,
sortable_values, strip_wildcard, timezone, unescape_wildcard)
from trytond.tools.domain_inversion import (
concat, domain_inversion, eval_domain, extract_reference_models,
localize_domain, merge, parse, prepare_reference_domain, simplify,
unique_value)
canonicalize, concat, domain_inversion, eval_domain,
extract_reference_models, localize_domain, merge, parse,
prepare_reference_domain, simplify, sort, unique_value)
from trytond.tools.immutabledict import ImmutableDict
from trytond.tools.logging import format_args
from trytond.tools.string_ import LazyString, StringPartitioned
@@ -1099,6 +1099,59 @@ class DomainInversionTestCase(unittest.TestCase):
extract_reference_models(domain, 'x'), {'model_A', 'model_B'})
self.assertEqual(extract_reference_models(domain, 'y'), set())
def test_sort(self):
"Test sorting domain"
for (domain, expected) in [
([], []),
(['AND'], ['AND']),
(['OR'], ['OR']),
([('foo', '=', 1)], [('foo', '=', 1)]),
([('foo', '=', 1000), ('foo', '=', 0)],
[('foo', '=', 0), ('foo', '=', 1000)]),
([('foo', '=', 1), ('foo', '=', None)],
[('foo', '=', None), ('foo', '=', 1)]),
([('foo', '=', 1), []], [[], ('foo', '=', 1)]),
([[[[('foo', '=', 1)]]], [[('foo', '=', 1)]]],
[[[('foo', '=', 1)]], [[[('foo', '=', 1)]]]]),
([[('foo', '=', 1), ('bar', '=', 2)],
[('foo', '=', 1), ('bar', '=', 2), ('baz', '=', 3)]],
[[('bar', '=', 2), ('baz', '=', 3), ('foo', '=', 1)],
[('bar', '=', 2), ('foo', '=', 1)]]),
(['OR', ('foo', '=', 1), ('foo', '=', None),
['AND', ('bar', '=', 2), ('baz', '=', 3)]],
['OR', ('foo', '=', None), ('foo', '=', 1),
['AND', ('bar', '=', 2), ('baz', '=', 3)]]),
]:
with self.subTest(domain=domain):
self.assertEqual(sort(domain), expected)
def test_canonicalize(self):
"Test domain canonicalization"
for (domain, expected) in [
([], []),
(['AND'], []),
(['OR'], []),
([('foo', '=', 1), ('bar', '=', 2)],
[('bar', '=', 2), ('foo', '=', 1)]),
([[('foo', '=', 1)]], [('foo', '=', 1)]),
(['AND', ['OR', ('bar', '=', 2), ('baz', '=', 3)],
('foo', '=', 1)],
[('foo', '=', 1),
['OR', ('bar', '=', 2), ('baz', '=', 3)]]),
(['OR', ['AND', ('bar', '=', 2), ('baz', '=', 3)],
('foo', '=', 1)],
['OR', ('foo', '=', 1),
[('bar', '=', 2), ('baz', '=', 3)]]),
(['AND', ['AND', ('bar', '=', 2), ('baz', '=', 3)],
('foo', '=', 1)],
[('bar', '=', 2), ('baz', '=', 3), ('foo', '=', 1)]),
(['OR', ['OR', ('bar', '=', 2), ('baz', '=', 3)],
('foo', '=', 1)],
['OR', ('bar', '=', 2), ('baz', '=', 3), ('foo', '=', 1)]),
]:
with self.subTest(domain=domain):
self.assertEqual(canonicalize(domain), expected)
@unittest.skipUnless(barcode, "required barcode")
class BarcodeTestCase(unittest.TestCase):
+47
View File
@@ -284,6 +284,34 @@ def localize_domain(domain, field_name=None, strip_target=False):
for part in domain]
def _sort_key(domain):
if not domain:
return (0, tuple())
elif is_leaf(domain):
if domain[2] is None:
domain = list(domain).copy()
domain[2] = float('-inf')
return (1, tuple(domain))
elif domain in ['AND', 'OR']:
return (0, domain)
else:
content = tuple(_sort_key(e) for e in domain)
nestedness = max(k[0] for k in content)
return (nestedness + 1, content)
def sort(domain):
"Sort a domain"
if not domain:
return domain
elif is_leaf(domain):
return domain
elif domain in ['AND', 'OR']:
return domain
else:
return sorted((sort(e) for e in domain), key=_sort_key)
def bool_operator(domain):
"Returns the boolean operator used by a domain"
bool_op = 'AND'
@@ -340,6 +368,16 @@ def simplify_duplicate(domain):
return dedup_branches
def simplify_AND(domain):
"""Remove useless ANDs"""
if is_leaf(domain):
return domain
elif domain == 'OR':
return domain
else:
return [simplify_AND(e) for e in domain if e != 'AND']
def simplify(domain):
"""Remove duplicate expressions and useless OR/AND"""
if is_leaf(domain):
@@ -350,6 +388,15 @@ def simplify(domain):
return simplify_nested(simplify_duplicate(domain))
def canonicalize(domain):
"""Returns the canonical version of a domain.
The canonical version of a domain is one where the domain is both
simplified and sorted
"""
return simplify_AND(sort(simplify(domain)))
def merge(domain, domoperator=None):
if not domain or domain in ('AND', 'OR'):
return []