diff --git a/README.md b/README.md index 917b720..3fda111 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,52 @@ The API is very easy to use. You can see this benchmark notebook for reference. pip install mercury-settrie ``` +## Usage + +```python +from settrie import SetTrie + +# Create a SetTrie object +stt = SetTrie() + +# Insert some sets +stt.insert({2, 3}, 'id1') +stt.insert({2, 3, 4.4}, 'id2') +stt.insert({'Mon', 'Tue'}, 'days') + +# Find id by set +print(stt.find({2, 3})) + +# Find ids of all supersets +for id in stt.supersets({2, 3}): + print(id) + +# Find ids of all subsets +for id in stt.subsets({2, 3}): + print(id) + +# Nested iteration over the sets and elements +for st in stt: + print(st.id) + for e in st.elements: + print(' ', e) + +# Store as a pickle file file +import pickle +with open('my_settrie.pickle', 'wb') as f: + pickle.dump(stt, f) + +# Load from a pickle file +with open('my_settrie.pickle', 'rb') as f: + tt = pickle.load(f) + +# Check that they are identical +for t, st in zip(tt, stt): + assert t.id == st.id + for et, est in zip(t.elements, st.elements): + assert et == est +``` + ## Clone and set up a development environment to work with it To work with Settrie command line or develop Settrie, you can set up an environment with git, gcc, make and the following tools: diff --git a/pyproject.toml b/pyproject.toml index 82ed5c8..5d2b0cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta' [project] name = 'mercury-settrie' -version = '1.4.3' +version = '1.4.4' description = 'A SetTrie is a container of sets that performs efficient subset and superset queries.' license = {file = "LICENSE.txt"} requires-python = '>=3.8' diff --git a/src/settrie/SetTrie.py b/src/settrie/SetTrie.py index 0c0d645..2ee5251 100644 --- a/src/settrie/SetTrie.py +++ b/src/settrie/SetTrie.py @@ -18,12 +18,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re + from . import new_settrie from . import destroy_settrie from . import insert from . import find from . import supersets from . import subsets +from . import elements +from . import next_set_id +from . import set_name from . import iterator_size from . import iterator_next from . import destroy_iterator @@ -33,7 +38,6 @@ from . import binary_image_next from . import destroy_binary_image - from typing import Set @@ -41,8 +45,13 @@ class Result: """ Container holding the results of several operations of SetTrie. It behaves, basically, like an iterator. """ - def __init__(self, iter_id): + def __init__(self, iter_id, auto_serialize = False): self.iter_id = iter_id + self.as_is = True + if auto_serialize: + self.as_is = False + self.to_string = re.compile("^'(.+)'$") + self.to_float = re.compile('^.*\\..*$') def __del__(self): destroy_iterator(self.iter_id) @@ -52,11 +61,45 @@ def __iter__(self): def __next__(self): if iterator_size(self.iter_id) > 0: - return iterator_next(self.iter_id) + if self.as_is: + return iterator_next(self.iter_id) + + s = iterator_next(self.iter_id) + + if self.to_string.match(s): + return self.to_string.sub('\\1', s) + + if self.to_float.match(s): + return float(s) + + return int(s) + else: raise StopIteration +class TreeSet: + """ Class returned by the iterator of SetTrie to simplify iterating over the elements + while not computing a list of strings (calling c++ elements()) unless it is required. + """ + def __init__(self, st_id, set_id): + self.st_id = st_id + self.set_id = set_id + + @property + def id(self): + return set_name(self.st_id, self.set_id) + + @property + def elements(self): + iid = elements(self.st_id, self.set_id) + + if iid == 0: + return None + + return Result(iid, auto_serialize=True) + + class SetTrie: """ Mapping container for efficient storage of key-value pairs where the keys are sets. Uses an efficient trie implementation. Supports querying @@ -64,18 +107,69 @@ class SetTrie: Example: ```python - >>> from mercury.dynamics.SetTrie import SetTrie - >>> s = SetTrie() - >>> s.insert({2,3}, 'id1') - >>> s.insert({2,3,4}, 'id2') + >>> from settrie import SetTrie + >>> + >>> # Create a SetTrie object + >>> stt = SetTrie() + >>> + >>> # Insert some sets + >>> stt.insert({2, 3}, 'id1') + >>> stt.insert({2, 3, 4.4}, 'id2') + >>> stt.insert({'Mon', 'Tue'}, 'days') + >>> + >>> # Find id by set + >>> print(stt.find({2, 3})) + >>> + >>> # Find ids of all supersets + >>> for id in stt.supersets({2, 3}): + >>> print(id) + >>> + >>> # Find ids of all subsets + >>> for id in stt.subsets({2, 3}): + >>> print(id) + >>> + >>> # Nested iteration over the sets and elements + >>> for st in stt: + >>> print(st.id) + >>> for e in st.elements: + >>> print(' ', e) + >>> + >>> # Store as a pickle file file + >>> import pickle + >>> with open('my_settrie.pickle', 'wb') as f: + >>> pickle.dump(stt, f) + >>> + >>> # Load from a pickle file + >>> with open('my_settrie.pickle', 'rb') as f: + >>> tt = pickle.load(f) + >>> + >>> # Check that they are identical + >>> for t, st in zip(tt, stt): + >>> assert t.id == st.id + >>> for et, est in zip(t.elements, st.elements): + >>> assert et == est ``` """ def __init__(self, binary_image=None): - self.st_id = new_settrie() - + self.st_id = new_settrie() + self.set_id = -1 if binary_image is not None: self.load_from_binary_image(binary_image) + def __iter__(self): + return self + + def __next__(self): + if self.set_id < 0: + self.set_id = -1 + + self.set_id = next_set_id(self.st_id, self.set_id) + + if self.set_id < 0: + raise StopIteration + + return TreeSet(self.st_id, self.set_id) + def __del__(self): destroy_settrie(self.st_id) @@ -95,7 +189,7 @@ def insert(self, set: Set, id: str): Args: set: Set to add - id: String representind the ID for the test + id: String representing the ID for the test """ insert(self.st_id, str(set), id) @@ -174,6 +268,8 @@ def load_from_binary_image(self, binary_image): if not failed: failed = not push_binary_image_block(self.st_id, '') + self.set_id = -1 + if failed: destroy_settrie(self.st_id) self.st_id = new_settrie() diff --git a/src/settrie/__init__.py b/src/settrie/__init__.py index 469fde6..af2a3f4 100644 --- a/src/settrie/__init__.py +++ b/src/settrie/__init__.py @@ -80,6 +80,15 @@ def supersets(st_id, set): def subsets(st_id, set): return _py_settrie.subsets(st_id, set) +def elements(st_id, set_id): + return _py_settrie.elements(st_id, set_id) + +def next_set_id(st_id, set_id): + return _py_settrie.next_set_id(st_id, set_id) + +def set_name(st_id, set_id): + return _py_settrie.set_name(st_id, set_id) + def iterator_size(iter_id): return _py_settrie.iterator_size(iter_id) @@ -106,7 +115,7 @@ def destroy_binary_image(image_id): # The source version file is /src/version.py, anything else is auto generated. -__version__ = '1.4.3' +__version__ = '1.4.4' from settrie.SetTrie import SetTrie from settrie.SetTrie import Result diff --git a/src/settrie/py_settrie.i b/src/settrie/py_settrie.i index 1193231..1ee9f44 100644 --- a/src/settrie/py_settrie.i +++ b/src/settrie/py_settrie.i @@ -7,6 +7,9 @@ extern char *find (int st_id, char *set); extern int supersets (int st_id, char *set); extern int subsets (int st_id, char *set); + extern int elements (int st_id, int set_id); + extern int next_set_id (int st_id, int set_id); + extern char *set_name (int st_id, int set_id); extern int iterator_size (int iter_id); extern char *iterator_next (int iter_id); extern void destroy_iterator (int iter_id); @@ -23,6 +26,9 @@ extern void insert (int st_id, char *set, char *str_id); extern char *find (int st_id, char *set); extern int supersets (int st_id, char *set); extern int subsets (int st_id, char *set); +extern int elements (int st_id, int set_id); +extern int next_set_id (int st_id, int set_id); +extern char *set_name (int st_id, int set_id); extern int iterator_size (int iter_id); extern char *iterator_next (int iter_id); extern void destroy_iterator (int iter_id); diff --git a/src/settrie/py_settrie_wrap.cpp b/src/settrie/py_settrie_wrap.cpp index 3af8233..571f7c4 100644 --- a/src/settrie/py_settrie_wrap.cpp +++ b/src/settrie/py_settrie_wrap.cpp @@ -2704,6 +2704,9 @@ static swig_module_info swig_module = {swig_types, 1, 0, 0, 0, 0}; extern char *find (int st_id, char *set); extern int supersets (int st_id, char *set); extern int subsets (int st_id, char *set); + extern int elements (int st_id, int set_id); + extern int next_set_id (int st_id, int set_id); + extern char *set_name (int st_id, int set_id); extern int iterator_size (int iter_id); extern char *iterator_next (int iter_id); extern void destroy_iterator (int iter_id); @@ -3215,6 +3218,96 @@ SWIGINTERN PyObject *_wrap_subsets(PyObject *SWIGUNUSEDPARM(self), PyObject *arg } +SWIGINTERN PyObject *_wrap_elements(PyObject *SWIGUNUSEDPARM(self), PyObject *args) { + PyObject *resultobj = 0; + int arg1 ; + int arg2 ; + int val1 ; + int ecode1 = 0 ; + int val2 ; + int ecode2 = 0 ; + PyObject *swig_obj[2] ; + int result; + + if (!SWIG_Python_UnpackTuple(args, "elements", 2, 2, swig_obj)) SWIG_fail; + ecode1 = SWIG_AsVal_int(swig_obj[0], &val1); + if (!SWIG_IsOK(ecode1)) { + SWIG_exception_fail(SWIG_ArgError(ecode1), "in method '" "elements" "', argument " "1"" of type '" "int""'"); + } + arg1 = (int)(val1); + ecode2 = SWIG_AsVal_int(swig_obj[1], &val2); + if (!SWIG_IsOK(ecode2)) { + SWIG_exception_fail(SWIG_ArgError(ecode2), "in method '" "elements" "', argument " "2"" of type '" "int""'"); + } + arg2 = (int)(val2); + result = (int)elements(arg1,arg2); + resultobj = SWIG_From_int((int)(result)); + return resultobj; +fail: + return NULL; +} + + +SWIGINTERN PyObject *_wrap_next_set_id(PyObject *SWIGUNUSEDPARM(self), PyObject *args) { + PyObject *resultobj = 0; + int arg1 ; + int arg2 ; + int val1 ; + int ecode1 = 0 ; + int val2 ; + int ecode2 = 0 ; + PyObject *swig_obj[2] ; + int result; + + if (!SWIG_Python_UnpackTuple(args, "next_set_id", 2, 2, swig_obj)) SWIG_fail; + ecode1 = SWIG_AsVal_int(swig_obj[0], &val1); + if (!SWIG_IsOK(ecode1)) { + SWIG_exception_fail(SWIG_ArgError(ecode1), "in method '" "next_set_id" "', argument " "1"" of type '" "int""'"); + } + arg1 = (int)(val1); + ecode2 = SWIG_AsVal_int(swig_obj[1], &val2); + if (!SWIG_IsOK(ecode2)) { + SWIG_exception_fail(SWIG_ArgError(ecode2), "in method '" "next_set_id" "', argument " "2"" of type '" "int""'"); + } + arg2 = (int)(val2); + result = (int)next_set_id(arg1,arg2); + resultobj = SWIG_From_int((int)(result)); + return resultobj; +fail: + return NULL; +} + + +SWIGINTERN PyObject *_wrap_set_name(PyObject *SWIGUNUSEDPARM(self), PyObject *args) { + PyObject *resultobj = 0; + int arg1 ; + int arg2 ; + int val1 ; + int ecode1 = 0 ; + int val2 ; + int ecode2 = 0 ; + PyObject *swig_obj[2] ; + char *result = 0 ; + + if (!SWIG_Python_UnpackTuple(args, "set_name", 2, 2, swig_obj)) SWIG_fail; + ecode1 = SWIG_AsVal_int(swig_obj[0], &val1); + if (!SWIG_IsOK(ecode1)) { + SWIG_exception_fail(SWIG_ArgError(ecode1), "in method '" "set_name" "', argument " "1"" of type '" "int""'"); + } + arg1 = (int)(val1); + ecode2 = SWIG_AsVal_int(swig_obj[1], &val2); + if (!SWIG_IsOK(ecode2)) { + SWIG_exception_fail(SWIG_ArgError(ecode2), "in method '" "set_name" "', argument " "2"" of type '" "int""'"); + } + arg2 = (int)(val2); + result = (char *)set_name(arg1,arg2); + resultobj = SWIG_FromCharPtr((const char *)result); + return resultobj; +fail: + return NULL; +} + + SWIGINTERN PyObject *_wrap_iterator_size(PyObject *SWIGUNUSEDPARM(self), PyObject *args) { PyObject *resultobj = 0; int arg1 ; @@ -3415,6 +3508,9 @@ static PyMethodDef SwigMethods[] = { { "find", _wrap_find, METH_VARARGS, NULL}, { "supersets", _wrap_supersets, METH_VARARGS, NULL}, { "subsets", _wrap_subsets, METH_VARARGS, NULL}, + { "elements", _wrap_elements, METH_VARARGS, NULL}, + { "next_set_id", _wrap_next_set_id, METH_VARARGS, NULL}, + { "set_name", _wrap_set_name, METH_VARARGS, NULL}, { "iterator_size", _wrap_iterator_size, METH_O, NULL}, { "iterator_next", _wrap_iterator_next, METH_O, NULL}, { "destroy_iterator", _wrap_destroy_iterator, METH_O, NULL}, diff --git a/src/settrie/settrie.cpp b/src/settrie/settrie.cpp index d03149e..b75cae9 100644 --- a/src/settrie/settrie.cpp +++ b/src/settrie/settrie.cpp @@ -239,7 +239,7 @@ String SetTrie::find (StringSet set) { int idx = find(b_set); - if (idx == 0 || !tree[idx].is_flaged) + if (idx == 0 || !tree[idx].is_flagged) return ""; return id[idx]; @@ -363,6 +363,28 @@ StringSet SetTrie::subsets (String str, char split) { } +StringSet SetTrie::elements (int idx) { + + StringSet ret = {}; + + if (idx > 0 && idx < tree.size() && tree[idx].is_flagged) { + String elem; + while (idx > 0) { + ElementHash hh = tree[idx].value; + + StringName::iterator it = name.find(hh); + + if (it != name.end()) + ret.push_back(it->second); + + idx = tree[idx].idx_parent; + } + } + + return ret; +} + + bool SetTrie::load (pBinaryImage &p_bi) { int c_block = 0, c_ofs = 0; @@ -763,7 +785,7 @@ char *find (int st_id, char *set) { \param set A Python set serialized by a str() call. \return 0 if no sets were found, or an iter_id > 0 that can be used to retrieve the result using iterator_next()/iterator_size() - and must be explicitely destroyed via destroy_iterator() + and must be explicitly destroyed via destroy_iterator() */ int supersets (int st_id, char *set) { @@ -789,7 +811,7 @@ int supersets (int st_id, char *set) { \param set A Python set serialized by a str() call. \return 0 if no sets were found, or an iter_id > 0 that can be used to retrieve the result using iterator_next()/iterator_size() - and must be explicitely destroyed via destroy_iterator() + and must be explicitly destroyed via destroy_iterator() */ int subsets (int st_id, char *set) { @@ -809,6 +831,98 @@ int subsets (int st_id, char *set) { } +/** Return all the elements in a set from a SetTrie identified by set_id as an iterator of strings. + + \param st_id The st_id returned by a previous new_settrie() call. + \param set_id A valid set_id returned by a successful next_set_id() call. + + \return 0 on error or the empty set, or an iter_id > 0 that can be used to retrieve the result using + iterator_next()/iterator_size() and must be explicitly destroyed via destroy_iterator() +*/ +int elements (int st_id, int set_id) { + + if (set_id == 0) + return 0; + + SetTrieServer::iterator it = instance.find(st_id); + + if (it == instance.end()) + return 0; + + StringSet ret = it->second->elements(set_id); + + if (ret.size() == 0) + return 0; + + iterator[++instance_iter] = new StringSet(ret); + + return instance_iter; +} + + +/** Return the integer set_id of the next set stored in a SetTrie after a given set_id to iterate over all the sets in the object. + + \param st_id The st_id returned by a previous new_settrie() call. + \param set_id A valid set_id returned by previous call or the constant -1 to return the first set. Note that 0 may be the set_id of + the empty set in case the empty set is in the SetTrie. + + \return A unique integer set_id that can be used for iterating, calling set_name() or elements(). On error, it will return -3 + if the st_id or the set_id is invalid and -2 if the set_id given is the last set in the object or the object is empty. +*/ +int next_set_id (int st_id, int set_id) { + + SetTrieServer::iterator it = instance.find(st_id); + + if (it == instance.end()) + return -3; + + if (set_id == -1) { + if (it->second->id.size() == 0) + return -2; + + IdMap::iterator jt = it->second->id.begin(); + + return jt->first; + } + + IdMap::iterator jt = it->second->id.find(set_id); + + if (jt == it->second->id.end()) + return -3; + + ++jt; + + if (jt == it->second->id.end()) + return -2; + + return jt->first; +} + + +/** Return the name (Python id) of a set stored in a SetTrie identified by its binary (int) set_id. + + \param st_id The st_id returned by a previous new_settrie() call. + \param set_id A valid set_id returned by a successful next_set_id() call. + + \return An empty string on any error and the Python id of the set if both st_id and set_id are valid. +*/ +char *set_name (int st_id, int set_id) { + + SetTrieServer::iterator it = instance.find(st_id); + + answer_buffer[0] = 0; + + if (it != instance.end()) { + IdMap::iterator jt = it->second->id.find(set_id); + + if (jt != it->second->id.end()) + strcpy(answer_buffer, jt->second.c_str()); + } + + return answer_buffer; +} + + /** Return the number of unread items in an iterator (returned by subsets() or supersets()). \param iter_id The iter_id returned by a previous subsets() or supersets() call. diff --git a/src/settrie/settrie.h b/src/settrie/settrie.h index b48b82d..fb4b87a 100644 --- a/src/settrie/settrie.h +++ b/src/settrie/settrie.h @@ -42,9 +42,9 @@ typedef std::map IdMap; struct SetNode { ElementHash value; - int idx_next, idx_child; + int idx_next, idx_child, idx_parent; - bool is_flaged; + bool is_flagged; }; typedef std::vector BinaryTree; @@ -65,7 +65,7 @@ class SetTrie { public: SetTrie() { - SetNode root = {0, 0, 0, false}; + SetNode root = {0, 0, 0, -1, false}; tree.push_back(root); } @@ -77,9 +77,12 @@ class SetTrie { StringSet supersets (String str, char split); StringSet subsets (StringSet set); StringSet subsets (String str, char split); + StringSet elements (int idx); bool load (pBinaryImage &p_bi); bool save (pBinaryImage &p_bi); + IdMap id = {}; + #ifndef TEST private: #endif @@ -87,8 +90,7 @@ class SetTrie { inline int insert(int idx, ElementHash value) { if (tree[idx].idx_child == 0) { - - SetNode node = {value, 0, 0, false}; + SetNode node = {value, 0, 0, idx, false}; tree.push_back(node); @@ -106,7 +108,7 @@ class SetTrie { return idx; if (tree[idx].idx_next == 0) { - SetNode node = {value, 0, 0, false}; + SetNode node = {value, 0, 0, tree[idx].idx_parent, false}; tree.push_back(node); @@ -127,7 +129,7 @@ class SetTrie { int size = set.size(); if (size == 0) { - tree[0].is_flaged = true; + tree[0].is_flagged = true; return 0; } @@ -135,7 +137,7 @@ class SetTrie { for (int i = 0; i < size; i++) idx = insert(idx, set[i]); - tree[idx].is_flaged = true; + tree[idx].is_flagged = true; return idx; } @@ -169,7 +171,7 @@ class SetTrie { inline void all_supersets(int t_idx) { while (t_idx != 0) { - if (tree[t_idx].is_flaged) + if (tree[t_idx].is_flagged) result.push_back(t_idx); if (int ci = tree[t_idx].idx_child) @@ -189,7 +191,7 @@ class SetTrie { if ((t_value = tree[t_idx].value) == (q_value = query[s_idx])) { if (s_idx == last_query_idx) { - if (tree[t_idx].is_flaged) + if (tree[t_idx].is_flagged) result.push_back(t_idx); if (int ci = tree[t_idx].idx_child) @@ -220,7 +222,7 @@ class SetTrie { ns_idx++; if (query[ns_idx] == t_value) { - if (tree[t_idx].is_flaged) + if (tree[t_idx].is_flagged) result.push_back(t_idx); int ni; @@ -243,6 +245,5 @@ class SetTrie { IdList result = {}; BinaryTree tree = {}; StringName name = {}; - IdMap id = {}; }; -#endif \ No newline at end of file +#endif diff --git a/src/test.sh b/src/test.sh index 6c38d86..0574ed6 100755 --- a/src/test.sh +++ b/src/test.sh @@ -1,2 +1,2 @@ -coverage run --omit 'settrie/__init__.py' -m pytest test_all.py +coverage run --omit /usr/lib/*,settrie/__init__.py -m pytest test_all.py coverage report -m diff --git a/src/test_all.py b/src/test_all.py index 90e0f07..4fbf513 100644 --- a/src/test_all.py +++ b/src/test_all.py @@ -20,7 +20,7 @@ import os, pickle -from settrie import SetTrie, destroy_settrie +from settrie import SetTrie, Result, destroy_settrie, next_set_id, elements, set_name def test_basic(): @@ -143,3 +143,115 @@ def test_force_errors(): s = SetTrie() assert not s.load_from_binary_image(['Load this, please.']) + + +def get_iterator_dataset(): + stt = SetTrie() + + names = ['integers', 'days', 'spanglish', 'días', 'more_integers', 'void', 'planets', 'pai'] + sets = [{1, 2, 3, 4}, {'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'}, + {'lunes', 'martes', 'Wednesday', 'Thursday', 'viernes'}, + {'lunes', 'martes', 'miércoles', 'jueves', 'viernes'}, {1, 2, 3, 4, 5}, {}, {'Earth', 'Mars', '...'}, {3.0, 3.1, 3.14}] + + for s, n in zip(sets, names): + stt.insert(s, n) + + return stt, names, sets + + +def test_nested_iterator_calls(): + stt, names, sets = get_iterator_dataset() + + assert next_set_id(54321, -1) == -3 + + st_id = stt.st_id + N = 0 + seen = [] + set_id = -1 + + assert next_set_id(st_id, 54321) == -3 + + st2 = SetTrie() + + assert next_set_id(st2.st_id, set_id) == -2 + + assert elements(54321, 0) == 0 + assert elements(st2.st_id, 0) == 0 + assert elements(st_id, 54321) == 0 + + while set_id != -2: + set_id = next_set_id(st_id, set_id) + + if set_id == -2: + break + + assert set_id >= 0 + + name = set_name(st_id, set_id) + assert name in names + assert name not in seen + seen.append(name) + + iix = elements(st_id, set_id) + + if name == 'void': + assert iix == 0 + else: + assert iix > 0 + + ee = Result(iix, True) + ix = names.index(name) + Ne = 0 + Le = [] + + for e in ee: + assert e in sets[ix] + assert e not in Le + Le.append(e) + Ne += 1 + + assert Ne == len(sets[ix]) + + N += 1 + + assert N == len(names) + + +def test_nested_iterators(): + stt, names, sets = get_iterator_dataset() + + N = 0 + seen = [] + + for st in stt: + assert st.id in names + assert st.id not in seen + seen.append(st.id) + + if st.id == 'void': + assert st.elements is None + else: + ix = names.index(st.id) + Ne = 0 + Le = [] + + for e in st.elements: + assert e in sets[ix] + assert e not in Le + Le.append(e) + Ne += 1 + + assert Ne == len(sets[ix]) + + N += 1 + + assert N == len(names) + + +# test_basic() +# test_one_page_save_load() +# test_multi_page_save_load() +# test_pickle_save_load() +# test_force_errors() +# test_nested_iterator_calls() +# test_nested_iterators() diff --git a/src/version.py b/src/version.py index c51d3bc..f6b8313 100644 --- a/src/version.py +++ b/src/version.py @@ -1,3 +1,3 @@ # The source version file is /src/version.py, anything else is auto generated. -__version__ = '1.4.3' +__version__ = '1.4.4'