├── .gitignore ├── .clang-format ├── scripts ├── install-git-hooks ├── pre-commit.hook └── commit-msg.hook ├── Makefile ├── tst.h ├── README.md ├── test_cpy.c ├── test_ref.c └── tst.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.o.d 3 | test_cpy 4 | test_ref 5 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Chromium 2 | Language: Cpp 3 | MaxEmptyLinesToKeep: 3 4 | IndentCaseLabels: false 5 | AllowShortIfStatementsOnASingleLine: false 6 | AllowShortCaseLabelsOnASingleLine: false 7 | AllowShortLoopsOnASingleLine: false 8 | DerivePointerAlignment: false 9 | PointerAlignment: Right 10 | SpaceAfterCStyleCast: true 11 | TabWidth: 4 12 | UseTab: Never 13 | IndentWidth: 4 14 | BreakBeforeBraces: Linux 15 | AccessModifierOffset: -4 16 | -------------------------------------------------------------------------------- /scripts/install-git-hooks: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ! test -d .git; then 4 | echo "Execute scripts/install-git-hooks in the top-level directory." 5 | exit 1 6 | fi 7 | 8 | ln -sf ../../scripts/pre-commit.hook .git/hooks/pre-commit || exit 1 9 | chmod +x .git/hooks/pre-commit 10 | 11 | ln -sf ../../scripts/commit-msg.hook .git/hooks/commit-msg || exit 1 12 | chmod +x .git/hooks/commit-msg 13 | 14 | touch .git/hooks/applied || exit 1 15 | 16 | echo 17 | echo "Git commit hooks are installed successfully." 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = \ 2 | test_cpy \ 3 | test_ref 4 | 5 | CFLAGS = -Wall -Werror -g 6 | 7 | # Control the build verbosity 8 | ifeq ("$(VERBOSE)","1") 9 | Q := 10 | VECHO = @true 11 | else 12 | Q := @ 13 | VECHO = @printf 14 | endif 15 | 16 | GIT_HOOKS := .git/hooks/applied 17 | 18 | .PHONY: all clean 19 | 20 | all: $(GIT_HOOKS) $(TESTS) 21 | 22 | $(GIT_HOOKS): 23 | @scripts/install-git-hooks 24 | @echo 25 | 26 | OBJS_LIB = \ 27 | tst.o 28 | 29 | OBJS := \ 30 | $(OBJS_LIB) \ 31 | test_cpy.o \ 32 | test_ref.o 33 | 34 | deps := $(OBJS:%.o=.%.o.d) 35 | 36 | test_%: test_%.o $(OBJS_LIB) 37 | $(VECHO) " LD\t$@\n" 38 | $(Q)$(CC) $(LDFLAGS) -o $@ $^ 39 | 40 | %.o: %.c 41 | $(VECHO) " CC\t$@\n" 42 | $(Q)$(CC) -o $@ $(CFLAGS) -c -MMD -MF .$@.d $< 43 | 44 | clean: 45 | $(RM) $(TESTS) $(OBJS) 46 | $(RM) $(deps) 47 | 48 | -include $(deps) 49 | -------------------------------------------------------------------------------- /scripts/pre-commit.hook: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ASTYLE_OPTS="--style=kr --indent=spaces=4" 4 | CPPCHECK_OPTS="-I. --error-exitcode=1 ." 5 | 6 | RETURN=0 7 | ASTYLE=$(which astyle) 8 | if [ $? -ne 0 ]; then 9 | echo "[!] astyle not installed. Unable to check source file format policy." >&2 10 | exit 1 11 | fi 12 | 13 | CPPCHECK=$(which cppcheck) 14 | if [ $? -ne 0 ]; then 15 | echo "[!] cppcheck not installed. Unable to perform static analysis." >&2 16 | exit 1 17 | fi 18 | 19 | DIFF=$(which colordiff) 20 | if [ $? -ne 0 ]; then 21 | DIFF=diff 22 | fi 23 | 24 | FILES=`git diff --cached --name-only --diff-filter=ACMR | grep -E "\.(c|cpp|h)$"` 25 | for FILE in $FILES; do 26 | nf=`git checkout-index --temp $FILE | cut -f 1` 27 | newfile=`mktemp /tmp/${nf}.XXXXXX` || exit 1 28 | $ASTYLE $ASTYLE_OPTS < $nf > $newfile 2>> /dev/null 29 | $DIFF -u -p -B "${nf}" "${newfile}" 30 | r=$? 31 | rm "${newfile}" 32 | rm "${nf}" 33 | if [ $r != 0 ] ; then 34 | echo "[!] $FILE does not follow the consistent coding style." >&2 35 | RETURN=1 36 | fi 37 | done 38 | 39 | if [ $RETURN -eq 1 ]; then 40 | echo "" >&2 41 | echo "Make sure you have run astyle as the following:" >&2 42 | echo " astyle $ASTYLE_OPTS --suffix=none $FILE" >&2 43 | echo 44 | fi 45 | 46 | # static analysis 47 | $CPPCHECK $CPPCHECK_OPTS >/dev/null 48 | if [ $? -ne 0 ]; then 49 | RETURN=1 50 | echo "" >&2 51 | echo "Fail to pass static analysis." >&2 52 | echo 53 | fi 54 | 55 | exit $RETURN 56 | -------------------------------------------------------------------------------- /tst.h: -------------------------------------------------------------------------------- 1 | #ifndef PREFIX_SEARCH_H 2 | #define PREFIX_SEARCH_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | /* forward declaration of ternary search tree */ 9 | typedef struct tst_node tst_node; 10 | 11 | /** tst_ins_del() ins/del copy or reference of 's' from ternary search tree. 12 | * insert all nodes required for 's' in tree at eqkid node of leaf. if 'del' 13 | * is non-zero deletes 's' from tree, otherwise insert 's' at node->eqkid 14 | * with node->key set to the nul-chracter after final node in search path. if 15 | * 'cpy' is non-zero allocate storage for 's', otherwise save pointer to 's'. 16 | * if 's' already exists in tree, increment node->refcnt. (to be used for del). 17 | * returns address of 's' in tree on successful insert (or on delete if refcnt 18 | * non-zero), NULL on allocation failure on insert, or on successful removal 19 | * of 's' from tree. 20 | */ 21 | void *tst_ins_del(tst_node **root, 22 | char *const *s, 23 | const int del, 24 | const int cpy); 25 | 26 | /** tst_search(), non-recursive find of a string in ternary tree. 27 | * returns pointer to 's' on success, NULL otherwise. 28 | */ 29 | void *tst_search(const tst_node *p, const char *s); 30 | 31 | /** tst_search_prefix() fills ptr array 'a' with words prefixed with 's'. 32 | * once the node containing the first prefix matching 's' is found 33 | * tst_suggest() is called to traverse the ternary_tree beginning 34 | * at the node filling 'a' with pointers to all words that contain 35 | * the prefix upto 'max' words updating 'n' with the number of words 36 | * in 'a'. a pointer to the first node is returned on success 37 | * NULL otherwise. 38 | */ 39 | void *tst_search_prefix(const tst_node *root, 40 | const char *s, 41 | char **a, 42 | int *n, 43 | const int max); 44 | 45 | /** tst_traverse_fn(), traverse tree calling 'fn' on each word. 46 | * prototype for 'fn' is void fn(const void *, void *). data can 47 | * be NULL if unused. 48 | * 49 | * The callback can be implemented as following: 50 | * 51 | * // print each word. 52 | * void print_word(const void *node, void *data) { 53 | * printf("%s\n", tst_get_string(node)); 54 | * } 55 | * 56 | * Then, invoke as "tst_traverse_fn (root, print_word, NULL);" 57 | */ 58 | void tst_traverse_fn(const tst_node *p, 59 | void(fn)(const void *, void *), 60 | void *data); 61 | 62 | /** free the ternary search tree rooted at p, data storage internal. */ 63 | void tst_free_all(tst_node *p); 64 | 65 | /** free the ternary search tree rooted at p, data storage external. */ 66 | void tst_free(tst_node *p); 67 | 68 | /** access functions tst_get_key(), tst_get_refcnt, & tst_get_string(). 69 | * provide access to struct members through opague pointers availale 70 | * to program. 71 | */ 72 | char tst_get_key(const tst_node *node); 73 | unsigned tst_get_refcnt(const tst_node *node); 74 | char *tst_get_string(const tst_node *node); 75 | 76 | #endif 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ternary Search Tree (tst) 2 | 3 | A ternary search tree is a binary search tree (bst) with an additional 4 | node pointer. 5 | 6 | There is a relative dearth of information available about ternary trees, 7 | and specifially, proper node-rotation when a node is deleted from the tree. 8 | The only examples for a node-delete found are simple deletes that leave the 9 | tree dirty by leaving a node, with or withoutsiblings, but having no valid 10 | middle (or equal) pointer. 11 | 12 | With a binary search tree each node contains a *left* and *right* node-pointer 13 | so a binary choice controls traversal. Either a *greater than* or *less than* 14 | choice. As the result of a comparison between the node-data and a reference 15 | either the left or right node-pointer is used to further descend. 16 | 17 | A ternary search tree adds a third (or middle) node. While you can still use 18 | the `left-middle-right` notation, ternary trees use a `low-equal-high` pointer 19 | verbiage. With each node holding a *key ID*, the node pointers for the this 20 | code uses a `lokid`, `eqkid`, and `hikid` pointer naming convention. If the 21 | difference between a refernence and the node key is negative (lower than), 22 | the `lokid` node is traversed, if they are equal, the `eqkid` node is followed 23 | or `hikid` is followed. 24 | 25 | In addition to the `node->key`, the node used in this example adds a 26 | *reference count* (a `node->refcnt`) to the node data to track the number of 27 | occurrances for each word it holds. So for example, if using the tree to track 28 | the words in an editor buffer (where there may be multiple occurrences of 'the' 29 | or other common words), the node holding 'the' is not deleted, upon delete, 30 | until no other occurrences remain (i.e. the `node->refcnt` is zero). 31 | 32 | Each individual node has the following form: 33 | ``` 34 | o 35 | |-key 36 | |-refcnt 37 | ------------+------------ 38 | |lokid |eqkid |hikid 39 | o o o 40 | ``` 41 | 42 | The string data (a pointer to a word, or a copy of the word itself) is stored 43 | in an additional special node following the node containing the last 44 | character (`node->key`) in the search path for the word, cast to type 45 | (node *) and stored as the `node->eqkid` pointer. Further, since this is 46 | the *node after the last character*, its key is the 47 | *nul-character* (decimal `0`) just as you would expect when ending a string. 48 | Thus, the 'key's for each of the nodes that make up the search path of a word, 49 | are the letters in the word with the final node having a key `0` with either 50 | a pointer to the string (if stored in an external data structure) or an 51 | allocated copy of the string itself if the string is to be stored in the 52 | tree. (as in holding the words for an edit buffer, where the location/address 53 | for the string changes with each keypress). In either case, the traversal down 54 | nodes to the final node will have a form similar to the following for the word 55 | "cat": 56 | ``` 57 | o 58 | |-c 59 | |-0 60 | ------------+------------ 61 | |lokid |eqkid |hikid 62 | o o o 63 | |-a 64 | |-0 65 | ----+---- 66 | | | | note: any of the lokid or hikid nodes 67 | o can also have pointers to nodes 68 | |-t for words that "cat" or "ca" is 69 | |-0 a partial prefix to. 70 | ----+---- 71 | | | | 72 | o 73 | |-0 74 | |-1 <== the refcnt is only relevant to the final node 75 | ----+---- 76 | | | | 77 | NULL o NULL 78 | "cat" 79 | ``` 80 | 81 | The ternary tree has the same O(n) efficiency for insert and search as does 82 | a bst. The delete is only slightly worse due to the proper deletion of the 83 | chain of unique nodes that make a word and proper rotation to eliminate the 84 | final node containing siblings. Lookup times associated with loading the 85 | entire `/usr/share/dict/words` file and searching range between 86 | `0.000002 - 0.000014` sec. However, the *prefix search* ability offered by 87 | the ternary search tree sets it apart from virtually all other data 88 | stuctures. While Tri/Radix trees can perform as well, their memory 89 | requirements are often 20 times more than a ternary tree. 90 | 91 | The benefit of a ternary tree for prefix searching of text lies in its 92 | ability to quickly traverse a tree of any size finding the node containing 93 | the last character in the wanted prefix. An in-order traversal of that node 94 | identifies all strings in the tree containing the prefix. 95 | -------------------------------------------------------------------------------- /test_cpy.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "tst.h" 7 | 8 | /** constants insert, delete, max word(s) & stack nodes */ 9 | enum { INS, DEL, WRDMAX = 256, STKMAX = 512, LMAX = 1024 }; 10 | #define REF INS 11 | #define CPY DEL 12 | 13 | /* timing helper function */ 14 | static double tvgetf(void) 15 | { 16 | struct timespec ts; 17 | double sec; 18 | 19 | clock_gettime(CLOCK_REALTIME, &ts); 20 | sec = ts.tv_nsec; 21 | sec /= 1e9; 22 | sec += ts.tv_sec; 23 | 24 | return sec; 25 | } 26 | 27 | /* simple trim '\n' from end of buffer filled by fgets */ 28 | static void rmcrlf(char *s) 29 | { 30 | size_t len = strlen(s); 31 | if (len && s[len - 1] == '\n') 32 | s[--len] = 0; 33 | } 34 | 35 | #define IN_FILE "cities.txt" 36 | 37 | int main(int argc, char **argv) 38 | { 39 | char word[WRDMAX] = ""; 40 | char *sgl[LMAX] = {NULL}; 41 | tst_node *root = NULL, *res = NULL; 42 | int rtn = 0, idx = 0, sidx = 0; 43 | FILE *fp = fopen(IN_FILE, "r"); 44 | double t1, t2; 45 | 46 | if (!fp) { /* prompt, open, validate file for reading */ 47 | fprintf(stderr, "error: file open failed '%s'.\n", argv[1]); 48 | return 1; 49 | } 50 | 51 | t1 = tvgetf(); 52 | while ((rtn = fscanf(fp, "%s", word)) != EOF) { 53 | char *p = word; 54 | if (!tst_ins_del(&root, &p, INS, CPY)) { 55 | fprintf(stderr, "error: memory exhausted, tst_insert.\n"); 56 | fclose(fp); 57 | return 1; 58 | } 59 | idx++; 60 | } 61 | t2 = tvgetf(); 62 | 63 | fclose(fp); 64 | printf("ternary_tree, loaded %d words in %.6f sec\n", idx, t2 - t1); 65 | 66 | for (;;) { 67 | char *p; 68 | printf( 69 | "\nCommands:\n" 70 | " a add word to the tree\n" 71 | " f find word in tree\n" 72 | " s search words matching prefix\n" 73 | " d delete word from the tree\n" 74 | " q quit, freeing all data\n\n" 75 | "choice: "); 76 | fgets(word, sizeof word, stdin); 77 | p = NULL; 78 | switch (*word) { 79 | case 'a': 80 | printf("enter word to add: "); 81 | if (!fgets(word, sizeof word, stdin)) { 82 | fprintf(stderr, "error: insufficient input.\n"); 83 | break; 84 | } 85 | rmcrlf(word); 86 | p = word; 87 | t1 = tvgetf(); 88 | res = tst_ins_del(&root, &p, INS, CPY); 89 | t2 = tvgetf(); 90 | if (res) { 91 | idx++; 92 | printf(" %s - inserted in %.6f sec. (%d words in tree)\n", 93 | (char *) res, t2 - t1, idx); 94 | } else 95 | printf(" %s - already exists in list.\n", (char *) res); 96 | break; 97 | case 'f': 98 | printf("find word in tree: "); 99 | if (!fgets(word, sizeof word, stdin)) { 100 | fprintf(stderr, "error: insufficient input.\n"); 101 | break; 102 | } 103 | rmcrlf(word); 104 | t1 = tvgetf(); 105 | res = tst_search(root, word); 106 | t2 = tvgetf(); 107 | if (res) 108 | printf(" found %s in %.6f sec.\n", (char *) res, t2 - t1); 109 | else 110 | printf(" %s not found.\n", word); 111 | break; 112 | case 's': 113 | printf("find words matching prefix (at least 1 char): "); 114 | if (!fgets(word, sizeof word, stdin)) { 115 | fprintf(stderr, "error: insufficient input.\n"); 116 | break; 117 | } 118 | rmcrlf(word); 119 | t1 = tvgetf(); 120 | res = tst_search_prefix(root, word, sgl, &sidx, LMAX); 121 | t2 = tvgetf(); 122 | if (res) { 123 | printf(" %s - searched prefix in %.6f sec\n\n", word, t2 - t1); 124 | for (int i = 0; i < sidx; i++) 125 | printf("suggest[%d] : %s\n", i, sgl[i]); 126 | } else 127 | printf(" %s - not found\n", word); 128 | break; 129 | case 'd': 130 | printf("enter word to del: "); 131 | if (!fgets(word, sizeof word, stdin)) { 132 | fprintf(stderr, "error: insufficient input.\n"); 133 | break; 134 | } 135 | rmcrlf(word); 136 | p = word; 137 | printf(" deleting %s\n", word); 138 | t1 = tvgetf(); 139 | res = tst_ins_del(&root, &p, DEL, CPY); 140 | t2 = tvgetf(); 141 | if (res) 142 | printf(" delete failed.\n"); 143 | else { 144 | printf(" deleted %s in %.6f sec\n", word, t2 - t1); 145 | idx--; 146 | } 147 | break; 148 | case 'q': 149 | tst_free_all(root); 150 | return 0; 151 | break; 152 | default: 153 | fprintf(stderr, "error: invalid selection.\n"); 154 | break; 155 | } 156 | } 157 | 158 | return 0; 159 | } 160 | -------------------------------------------------------------------------------- /test_ref.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "tst.h" 7 | 8 | /** constants insert, delete, max word(s) & stack nodes */ 9 | enum { INS, DEL, WRDMAX = 256, STKMAX = 512, LMAX = 1024 }; 10 | #define REF INS 11 | #define CPY DEL 12 | 13 | /* timing helper function */ 14 | static double tvgetf(void) 15 | { 16 | struct timespec ts; 17 | double sec; 18 | 19 | clock_gettime(CLOCK_REALTIME, &ts); 20 | sec = ts.tv_nsec; 21 | sec /= 1e9; 22 | sec += ts.tv_sec; 23 | 24 | return sec; 25 | } 26 | 27 | /* simple trim '\n' from end of buffer filled by fgets */ 28 | static void rmcrlf(char *s) 29 | { 30 | size_t len = strlen(s); 31 | if (len && s[len - 1] == '\n') 32 | s[--len] = 0; 33 | } 34 | 35 | #define IN_FILE "cities.txt" 36 | 37 | int main(int argc, char **argv) 38 | { 39 | char word[WRDMAX] = ""; 40 | char *sgl[LMAX] = {NULL}; 41 | tst_node *root = NULL, *res = NULL; 42 | int rtn = 0, idx = 0, sidx = 0; 43 | FILE *fp = fopen(IN_FILE, "r"); 44 | double t1, t2; 45 | 46 | if (!fp) { /* prompt, open, validate file for reading */ 47 | fprintf(stderr, "error: file open failed '%s'.\n", argv[1]); 48 | return 1; 49 | } 50 | 51 | t1 = tvgetf(); 52 | while ((rtn = fscanf(fp, "%s", word)) != EOF) { 53 | char *p = word; 54 | /* FIXME: insert reference to each string */ 55 | if (!tst_ins_del(&root, &p, INS, CPY)) { 56 | fprintf(stderr, "error: memory exhausted, tst_insert.\n"); 57 | fclose(fp); 58 | return 1; 59 | } 60 | idx++; 61 | } 62 | t2 = tvgetf(); 63 | 64 | fclose(fp); 65 | printf("ternary_tree, loaded %d words in %.6f sec\n", idx, t2 - t1); 66 | 67 | for (;;) { 68 | char *p; 69 | printf( 70 | "\nCommands:\n" 71 | " a add word to the tree\n" 72 | " f find word in tree\n" 73 | " s search words matching prefix\n" 74 | " d delete word from the tree\n" 75 | " q quit, freeing all data\n\n" 76 | "choice: "); 77 | fgets(word, sizeof word, stdin); 78 | p = NULL; 79 | switch (*word) { 80 | case 'a': 81 | printf("enter word to add: "); 82 | if (!fgets(word, sizeof word, stdin)) { 83 | fprintf(stderr, "error: insufficient input.\n"); 84 | break; 85 | } 86 | rmcrlf(word); 87 | p = word; 88 | t1 = tvgetf(); 89 | /* FIXME: insert reference to each string */ 90 | res = tst_ins_del(&root, &p, INS, CPY); 91 | t2 = tvgetf(); 92 | if (res) { 93 | idx++; 94 | printf(" %s - inserted in %.6f sec. (%d words in tree)\n", 95 | (char *) res, t2 - t1, idx); 96 | } else 97 | printf(" %s - already exists in list.\n", (char *) res); 98 | break; 99 | case 'f': 100 | printf("find word in tree: "); 101 | if (!fgets(word, sizeof word, stdin)) { 102 | fprintf(stderr, "error: insufficient input.\n"); 103 | break; 104 | } 105 | rmcrlf(word); 106 | t1 = tvgetf(); 107 | res = tst_search(root, word); 108 | t2 = tvgetf(); 109 | if (res) 110 | printf(" found %s in %.6f sec.\n", (char *) res, t2 - t1); 111 | else 112 | printf(" %s not found.\n", word); 113 | break; 114 | case 's': 115 | printf("find words matching prefix (at least 1 char): "); 116 | if (!fgets(word, sizeof word, stdin)) { 117 | fprintf(stderr, "error: insufficient input.\n"); 118 | break; 119 | } 120 | rmcrlf(word); 121 | t1 = tvgetf(); 122 | res = tst_search_prefix(root, word, sgl, &sidx, LMAX); 123 | t2 = tvgetf(); 124 | if (res) { 125 | printf(" %s - searched prefix in %.6f sec\n\n", word, t2 - t1); 126 | for (int i = 0; i < sidx; i++) 127 | printf("suggest[%d] : %s\n", i, sgl[i]); 128 | } else 129 | printf(" %s - not found\n", word); 130 | break; 131 | case 'd': 132 | printf("enter word to del: "); 133 | if (!fgets(word, sizeof word, stdin)) { 134 | fprintf(stderr, "error: insufficient input.\n"); 135 | break; 136 | } 137 | rmcrlf(word); 138 | p = word; 139 | printf(" deleting %s\n", word); 140 | t1 = tvgetf(); 141 | /* FIXME: remove reference to each string */ 142 | res = tst_ins_del(&root, &p, DEL, CPY); 143 | t2 = tvgetf(); 144 | if (res) 145 | printf(" delete failed.\n"); 146 | else { 147 | printf(" deleted %s in %.6f sec\n", word, t2 - t1); 148 | idx--; 149 | } 150 | break; 151 | case 'q': 152 | tst_free_all(root); 153 | return 0; 154 | break; 155 | default: 156 | fprintf(stderr, "error: invalid selection.\n"); 157 | break; 158 | } 159 | } 160 | 161 | return 0; 162 | } 163 | -------------------------------------------------------------------------------- /scripts/commit-msg.hook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # git-good-commit(1) - Git hook to help you write good commit messages. 4 | # Released under the MIT License. 5 | # 6 | # https://github.com/tommarshall/git-good-commit 7 | 8 | COMMIT_MSG_FILE="$1" 9 | COMMIT_MSG_LINES= 10 | HOOK_EDITOR= 11 | SKIP_DISPLAY_WARNINGS=0 12 | WARNINGS= 13 | 14 | RED= 15 | YELLOW= 16 | BLUE= 17 | WHITE= 18 | CYAN= 19 | NC= 20 | 21 | # 22 | # Set colour variables if the output should be coloured. 23 | # 24 | 25 | set_colors() { 26 | local default_color=$(git config --get hooks.goodcommit.color || git config --get color.ui || echo 'auto') 27 | if [[ $default_color == 'always' ]] || [[ $default_color == 'auto' && -t 1 ]]; then 28 | RED='\033[1;31m' 29 | YELLOW='\033[1;33m' 30 | BLUE='\033[1;34m' 31 | WHITE='\033[1;37m' 32 | CYAN='\033[1;36m' 33 | NC='\033[0m' # No Color 34 | fi 35 | } 36 | 37 | # 38 | # Set the hook editor, using the same approach as git. 39 | # 40 | 41 | set_editor() { 42 | # $GIT_EDITOR appears to always be set to `:` when the hook is executed by Git? 43 | # ref: http://stackoverflow.com/q/41468839/885540 44 | # ref: https://github.com/tommarshall/git-good-commit/issues/11 45 | # HOOK_EDITOR=$GIT_EDITOR 46 | test -z "${HOOK_EDITOR}" && HOOK_EDITOR=$(git config --get core.editor) 47 | test -z "${HOOK_EDITOR}" && HOOK_EDITOR=$VISUAL 48 | test -z "${HOOK_EDITOR}" && HOOK_EDITOR=$EDITOR 49 | test -z "${HOOK_EDITOR}" && HOOK_EDITOR='vi' 50 | } 51 | 52 | # 53 | # Output prompt help information. 54 | # 55 | 56 | prompt_help() { 57 | echo -e "${RED}$(cat <<-EOF 58 | How to Write a Git Commit Message: https://chris.beams.io/posts/git-commit/ 59 | e - edit commit message 60 | n - abort commit 61 | ? - print help 62 | EOF 63 | )${NC}" 64 | } 65 | 66 | # 67 | # Add a warning with and . 68 | # 69 | 70 | add_warning() { 71 | local line_number=$1 72 | local warning=$2 73 | WARNINGS[$line_number]="${WARNINGS[$line_number]}$warning;" 74 | } 75 | 76 | # 77 | # Output warnings. 78 | # 79 | 80 | display_warnings() { 81 | if [ $SKIP_DISPLAY_WARNINGS -eq 1 ]; then 82 | # if the warnings were skipped then they should be displayed next time 83 | SKIP_DISPLAY_WARNINGS=0 84 | return 85 | fi 86 | 87 | for i in "${!WARNINGS[@]}"; do 88 | printf "%-74s ${WHITE}%s${NC}\n" "${COMMIT_MSG_LINES[$(($i-1))]}" "[line ${i}]" 89 | IFS=';' read -ra WARNINGS_ARRAY <<< "${WARNINGS[$i]}" 90 | for ERROR in "${WARNINGS_ARRAY[@]}"; do 91 | echo -e " ${YELLOW}- ${ERROR}${NC}" 92 | done 93 | done 94 | } 95 | 96 | # 97 | # Read the contents of the commit msg into an array of lines. 98 | # 99 | 100 | read_commit_message() { 101 | # reset commit_msg_lines 102 | COMMIT_MSG_LINES=() 103 | 104 | # read commit message into lines array 105 | while IFS= read -r; do 106 | 107 | # trim trailing spaces from commit lines 108 | shopt -s extglob 109 | REPLY="${REPLY%%*( )}" 110 | shopt -u extglob 111 | 112 | # ignore comments 113 | [[ $REPLY =~ ^# ]] 114 | test $? -eq 0 || COMMIT_MSG_LINES+=("$REPLY") 115 | 116 | done < <(cat $COMMIT_MSG_FILE) 117 | } 118 | 119 | # 120 | # Validate the contents of the commmit msg agains the good commit guidelines. 121 | # 122 | 123 | validate_commit_message() { 124 | # reset warnings 125 | WARNINGS=() 126 | 127 | # capture the subject, and remove the 'squash! ' prefix if present 128 | COMMIT_SUBJECT=${COMMIT_MSG_LINES[0]/#squash! /} 129 | 130 | # if the commit is empty there's nothing to validate, we can return here 131 | COMMIT_MSG_STR="${COMMIT_MSG_LINES[*]}" 132 | test -z "${COMMIT_MSG_STR[*]// }" && return; 133 | 134 | # if the commit subject starts with 'fixup! ' there's nothing to validate, we can return here 135 | [[ $COMMIT_SUBJECT == 'fixup! '* ]] && return; 136 | 137 | # 1. Separate subject from body with a blank line 138 | # ------------------------------------------------------------------------------ 139 | 140 | test ${#COMMIT_MSG_LINES[@]} -lt 1 || test -z "${COMMIT_MSG_LINES[1]}" 141 | test $? -eq 0 || add_warning 2 "Separate subject from body with a blank line" 142 | 143 | # 2. Limit the subject line to 50 characters 144 | # ------------------------------------------------------------------------------ 145 | 146 | test "${#COMMIT_SUBJECT}" -le 50 147 | test $? -eq 0 || add_warning 1 "Limit the subject line to 50 characters (${#COMMIT_SUBJECT} chars)" 148 | 149 | # 3. Capitalize the subject line 150 | # ------------------------------------------------------------------------------ 151 | 152 | [[ ${COMMIT_SUBJECT} =~ ^[[:blank:]]*([[:upper:]]{1}[[:lower:]]*|[[:digit:]]+)([[:blank:]]|[[:punct:]]|$) ]] 153 | test $? -eq 0 || add_warning 1 "Capitalize the subject line" 154 | 155 | # 4. Do not end the subject line with a period 156 | # ------------------------------------------------------------------------------ 157 | 158 | [[ ${COMMIT_SUBJECT} =~ [^\.]$ ]] 159 | test $? -eq 0 || add_warning 1 "Do not end the subject line with a period" 160 | 161 | # 5. Use the imperative mood in the subject line 162 | # ------------------------------------------------------------------------------ 163 | 164 | IMPERATIVE_MOOD_BLACKLIST=( 165 | added adds adding 166 | adjusted adjusts adjusting 167 | amended amends amending 168 | avoided avoids avoiding 169 | bumped bumps bumping 170 | changed changes changing 171 | checked checks checking 172 | committed commits committing 173 | copied copies copying 174 | corrected corrects correcting 175 | created creates creating 176 | decreased decreases decreasing 177 | deleted deletes deleting 178 | disabled disables disabling 179 | dropped drops dropping 180 | duplicated duplicates duplicating 181 | enabled enables enabling 182 | excluded excludes excluding 183 | fixed fixes fixing 184 | handled handles handling 185 | implemented implements implementing 186 | improved improves improving 187 | included includes including 188 | increased increases increasing 189 | installed installs installing 190 | introduced introduces introducing 191 | merged merges merging 192 | moved moves moving 193 | pruned prunes pruning 194 | refactored refactors refactoring 195 | released releases releasing 196 | removed removes removing 197 | renamed renames renaming 198 | replaced replaces replacing 199 | resolved resolves resolving 200 | reverted reverts reverting 201 | showed shows showing 202 | tested tests testing 203 | tidied tidies tidying 204 | updated updates updating 205 | used uses using 206 | ) 207 | 208 | # enable case insensitive match 209 | shopt -s nocasematch 210 | 211 | for BLACKLISTED_WORD in "${IMPERATIVE_MOOD_BLACKLIST[@]}"; do 212 | [[ ${COMMIT_SUBJECT} =~ ^[[:blank:]]*$BLACKLISTED_WORD ]] 213 | test $? -eq 0 && add_warning 1 "Use the imperative mood in the subject line, e.g 'fix' not 'fixes'" && break 214 | done 215 | 216 | # disable case insensitive match 217 | shopt -u nocasematch 218 | 219 | # 6. Wrap the body at 72 characters 220 | # ------------------------------------------------------------------------------ 221 | 222 | URL_REGEX='^[[:blank:]]*(https?|ftp|file)://[-A-Za-z0-9\+&@#/%?=~_|!:,.;]*[-A-Za-z0-9\+&@#/%=~_|]' 223 | 224 | for i in "${!COMMIT_MSG_LINES[@]}"; do 225 | LINE_NUMBER=$((i+1)) 226 | test "${#COMMIT_MSG_LINES[$i]}" -le 72 || [[ ${COMMIT_MSG_LINES[$i]} =~ $URL_REGEX ]] 227 | test $? -eq 0 || add_warning $LINE_NUMBER "Wrap the body at 72 characters (${#COMMIT_MSG_LINES[$i]} chars)" 228 | done 229 | 230 | # 7. Use the body to explain what and why vs. how 231 | # ------------------------------------------------------------------------------ 232 | 233 | # ? 234 | 235 | # 8. Do no write single worded commits 236 | # ------------------------------------------------------------------------------ 237 | 238 | COMMIT_SUBJECT_WORDS=(${COMMIT_SUBJECT}) 239 | test "${#COMMIT_SUBJECT_WORDS[@]}" -gt 1 240 | test $? -eq 0 || add_warning 1 "Do no write single worded commits" 241 | 242 | # 9. Do not start the subject line with whitespace 243 | # ------------------------------------------------------------------------------ 244 | 245 | [[ ${COMMIT_SUBJECT} =~ ^[[:blank:]]+ ]] 246 | test $? -eq 1 || add_warning 1 "Do not start the subject line with whitespace" 247 | } 248 | 249 | # 250 | # It's showtime. 251 | # 252 | 253 | set_colors 254 | 255 | set_editor 256 | 257 | if tty >/dev/null 2>&1; then 258 | TTY=$(tty) 259 | else 260 | TTY=/dev/tty 261 | fi 262 | 263 | while true; do 264 | 265 | read_commit_message 266 | 267 | validate_commit_message 268 | 269 | # if there are no WARNINGS are empty then we're good to break out of here 270 | test ${#WARNINGS[@]} -eq 0 && exit 0; 271 | 272 | display_warnings 273 | 274 | # Ask the question (not using "read -p" as it uses stderr not stdout) 275 | echo -en "${CYAN}Proceed with commit? [e/n/?] ${NC}" 276 | 277 | # Read the answer 278 | read REPLY < "$TTY" 279 | 280 | # Check if the reply is valid 281 | case "$REPLY" in 282 | E*|e*) $HOOK_EDITOR "$COMMIT_MSG_FILE" < $TTY; continue ;; 283 | N*|n*) exit 1 ;; 284 | *) SKIP_DISPLAY_WARNINGS=1; prompt_help; continue ;; 285 | esac 286 | 287 | done 288 | -------------------------------------------------------------------------------- /tst.c: -------------------------------------------------------------------------------- 1 | #include "tst.h" 2 | 3 | /** max word length to store in ternary search tree, stack size */ 4 | #define WRDMAX 128 5 | #define STKMAX (WRDMAX * 2) 6 | 7 | /** ternary search tree node. */ 8 | typedef struct tst_node { 9 | char key; /* char key for node (null for node with string) */ 10 | unsigned refcnt; /* refcnt tracks occurrence of word (for delete) */ 11 | struct tst_node *lokid; /* ternary low child pointer */ 12 | struct tst_node *eqkid; /* ternary equal child pointer */ 13 | struct tst_node *hikid; /* ternary high child pointer */ 14 | } tst_node; 15 | 16 | /** struct to use for static stack to remove nodes. */ 17 | typedef struct tst_stack { 18 | void *data[STKMAX]; 19 | size_t idx; 20 | } tst_stack; 21 | 22 | /** stack push/pop to store node pointers to delete word from tree. 23 | * on delete, store all nodes from root to leaf containing word to 24 | * allow word removal and reordering of tree. 25 | */ 26 | static void *tst_stack_push(tst_stack *s, void *node) 27 | { 28 | if (s->idx >= STKMAX) 29 | return NULL; 30 | 31 | return (s->data[(s->idx)++] = node); 32 | } 33 | 34 | static void *tst_stack_pop(tst_stack *s) 35 | { 36 | if (!s->idx) 37 | return NULL; 38 | 39 | void *node = s->data[--(s->idx)]; 40 | s->data[s->idx] = NULL; 41 | 42 | return node; 43 | } 44 | 45 | /** delete current data-node and parent, update 'node' to new parent. 46 | * before delete the current refcnt is checked, if non-zero, occurrences 47 | * of the word remain in buffer the node is not deleted, if refcnt zero, 48 | * the node is deleted. if 'freeword = 1' the copy of word allocated and 49 | * stored as the node->eqkid is freed, if 'freeword = 0', node->eqkid is 50 | * stored elsewhere and not freed, root node updated if changed. returns 51 | * NULL on success (deleted), otherwise returns the address of victim 52 | * if refcnt non-zero. 53 | */ 54 | static void *tst_del_word(tst_node **root, 55 | tst_node *node, 56 | tst_stack *stk, 57 | const int freeword) 58 | { 59 | tst_node *victim = node; /* begin deletion w/victim */ 60 | tst_node *parent = tst_stack_pop(stk); /* parent to victim */ 61 | 62 | if (!victim->refcnt) { /* if last occurrence */ 63 | if (!victim->key && freeword) /* check key is nul */ 64 | free(victim->eqkid); /* free string (data) */ 65 | 66 | /* remove unique suffix chain - parent & victim nodes 67 | * have no children. simple remove until the first parent 68 | * found with children. 69 | */ 70 | while (!parent->lokid && !parent->hikid && !victim->lokid && 71 | !victim->hikid) { 72 | parent->eqkid = NULL; 73 | free(victim); 74 | victim = parent; 75 | parent = tst_stack_pop(stk); 76 | if (!parent) { /* last word & root node */ 77 | free(victim); 78 | return (void *) (*root = NULL); 79 | } 80 | } 81 | 82 | /* check if victim is prefix for others (victim has lo/hi node). 83 | * if both lo & hi children, check if lokid->hikid present, if not, 84 | * move hikid to lokid->hikid, replace node with lokid and free node. 85 | * if lokid->hikid present, check hikid->lokid. If not present, then 86 | * move lokid to hikid->lokid, replace node with hikid free node. 87 | */ 88 | if (victim->lokid && victim->hikid) { /* victim has both lokid/hikid */ 89 | if (!victim->lokid->hikid) { /* check for hikid in lo tree */ 90 | /* rotate victim->hikid to victim->lokid->hikid, and 91 | * rotate victim->lokid to place of victim. 92 | */ 93 | victim->lokid->hikid = victim->hikid; 94 | if (!parent) 95 | *root = victim->lokid; 96 | else if (victim == parent->lokid) 97 | parent->lokid = victim->lokid; 98 | else if (victim == parent->hikid) 99 | parent->hikid = victim->lokid; 100 | else 101 | parent->eqkid = victim->lokid; 102 | free(victim); 103 | victim = NULL; 104 | } else if (!victim->hikid->lokid) { /* check for lokid in hi tree */ 105 | /* opposite rotation */ 106 | victim->hikid->lokid = victim->lokid; 107 | if (!parent) 108 | *root = victim->hikid; 109 | else if (victim == parent->lokid) 110 | parent->lokid = victim->hikid; 111 | else if (victim == parent->hikid) 112 | parent->hikid = victim->hikid; 113 | else 114 | parent->eqkid = victim->hikid; 115 | free(victim); 116 | victim = NULL; 117 | } else /* can't rotate, return, leaving victim->eqkid NULL */ 118 | return NULL; 119 | } else if (victim->lokid) { /* only lokid, replace victim with lokid */ 120 | parent->eqkid = victim->lokid; 121 | free(victim); 122 | victim = NULL; 123 | } else if (victim->hikid) { /* only hikid, replace victim with hikid */ 124 | parent->eqkid = victim->hikid; 125 | free(victim); 126 | victim = NULL; 127 | } else { /* victim - no children, but parent has other children */ 128 | if (victim == parent->lokid) { /* if parent->lokid - trim */ 129 | parent->lokid = NULL; 130 | free(victim); 131 | victim = NULL; 132 | } else if (victim == parent->hikid) { /* if parent->hikid - trim */ 133 | parent->hikid = NULL; 134 | free(victim); 135 | victim = NULL; 136 | } else { /* victim was parent->eqkid, but parent->lo/hikid exists */ 137 | parent->eqkid = NULL; /* set eqkid NULL */ 138 | free(victim); /* free current victim */ 139 | victim = parent; /* set parent = victim */ 140 | parent = tst_stack_pop(stk); /* get new parent */ 141 | /* if both victim hi/lokid are present */ 142 | if (victim->lokid && victim->hikid) { 143 | /* same checks and rotations as above */ 144 | if (!victim->lokid->hikid) { 145 | victim->lokid->hikid = victim->hikid; 146 | if (!parent) 147 | *root = victim->lokid; 148 | else if (victim == parent->lokid) 149 | parent->lokid = victim->lokid; 150 | else if (victim == parent->hikid) 151 | parent->hikid = victim->lokid; 152 | else 153 | parent->eqkid = victim->lokid; 154 | free(victim); 155 | victim = NULL; 156 | } else if (!victim->hikid->lokid) { 157 | victim->hikid->lokid = victim->lokid; 158 | if (!parent) 159 | *root = victim->hikid; 160 | else if (victim == parent->lokid) 161 | parent->lokid = victim->hikid; 162 | else if (victim == parent->hikid) 163 | parent->hikid = victim->hikid; 164 | else 165 | parent->eqkid = victim->hikid; 166 | free(victim); 167 | victim = NULL; 168 | } else 169 | return NULL; 170 | } 171 | /* if only lokid, rewire to parent */ 172 | else if (victim->lokid) { 173 | if (parent) { /* if parent exists, rewire */ 174 | if (victim == parent->lokid) 175 | parent->lokid = victim->lokid; 176 | else if (victim == parent->hikid) 177 | parent->hikid = victim->lokid; 178 | else 179 | parent->eqkid = victim->lokid; 180 | } else /* we are new root node, update root */ 181 | *root = victim->lokid; /* make last node root */ 182 | free(victim); 183 | victim = NULL; 184 | } 185 | /* if only hikid, rewire to parent */ 186 | else if (victim->hikid) { 187 | if (parent) { /* if parent exists, rewire */ 188 | if (victim == parent->lokid) 189 | parent->lokid = victim->hikid; 190 | else if (victim == parent->hikid) 191 | parent->hikid = victim->hikid; 192 | else 193 | parent->eqkid = victim->hikid; 194 | } else /* we are new root node, update root */ 195 | *root = victim->hikid; /* make last node root */ 196 | free(victim); 197 | victim = NULL; 198 | } 199 | } 200 | } 201 | } else /* node->refcnt non-zero */ 202 | printf(" %s (refcnt: %u) not removed.\n", (char *) node->eqkid, 203 | node->refcnt); 204 | 205 | return victim; /* return NULL on successful free, *node otherwise */ 206 | } 207 | 208 | /** tst_ins_del() ins/del copy or reference of 's' from ternary search tree. 209 | * insert all nodes required for 's' in tree at eqkid node of leaf. if 'del' 210 | * is non-zero deletes 's' from tree, otherwise insert 's' at node->eqkid 211 | * with node->key set to the nul-character after final node in search path. if 212 | * 'cpy' is non-zero allocate storage for 's', otherwise save pointer to 's'. 213 | * if 's' already exists in tree, increment node->refcnt. (to be used for del). 214 | * returns address of 's' in tree on successful insert (or on delete if refcnt 215 | * non-zero), NULL on allocation failure on insert, or on successful removal 216 | * of 's' from tree. 217 | */ 218 | void *tst_ins_del(tst_node **root, char *const *s, const int del, const int cpy) 219 | { 220 | int diff; 221 | const char *p = *s; 222 | tst_stack stk = {.data = {NULL}, .idx = 0}; 223 | tst_node *curr, **pcurr; 224 | 225 | if (!root || !*s) 226 | return NULL; /* validate parameters */ 227 | if (strlen(*s) + 1 > STKMAX / 2) /* limit length to 1/2 STKMAX */ 228 | return NULL; /* 128 char word length is plenty */ 229 | 230 | pcurr = root; /* start at root */ 231 | while ((curr = *pcurr)) { /* iterate to insertion node */ 232 | diff = *p - curr->key; /* get ASCII diff for >, <, = */ 233 | if (diff == 0) { /* if char equal to node->key */ 234 | if (*p++ == 0) { /* check if word is duplicate */ 235 | if (del) { /* delete instead of insert */ 236 | (curr->refcnt)--; /* decrement reference count */ 237 | /* chk refcnt, del 's', return NULL on successful del */ 238 | return tst_del_word(root, curr, &stk, 1); 239 | } else 240 | curr->refcnt++; /* increment refcnt if word exists */ 241 | return (void *) curr->eqkid; /* pointer to word / NULL on del */ 242 | } 243 | pcurr = &(curr->eqkid); /* get next eqkid pointer address */ 244 | } else if (diff < 0) { /* if char less than node->key */ 245 | pcurr = &(curr->lokid); /* get next lokid pointer address */ 246 | } else { /* if char greater than node->key */ 247 | pcurr = &(curr->hikid); /* get next hikid pointer address */ 248 | } 249 | if (del) 250 | tst_stack_push(&stk, curr); /* push node on stack for del */ 251 | } 252 | 253 | /* if not duplicate, insert remaining chars into tree rooted at curr */ 254 | for (;;) { 255 | /* allocate memory for node, and fill. use calloc (or include 256 | * string.h and initialize w/memset) to avoid valgrind warning 257 | * "Conditional jump or move depends on uninitialised value(s)" 258 | */ 259 | if (!(*pcurr = calloc(1, sizeof **pcurr))) { 260 | fprintf(stderr, "error: tst_insert(), memory exhausted.\n"); 261 | return NULL; 262 | } 263 | curr = *pcurr; 264 | curr->key = *p; 265 | curr->refcnt = 1; 266 | curr->lokid = curr->hikid = curr->eqkid = NULL; 267 | 268 | /* Place nodes until end of the string, at end of stign allocate 269 | * space for data, copy data as final eqkid, and return. 270 | */ 271 | if (*p++ == 0) { 272 | if (cpy) { /* allocate storage for 's' */ 273 | const char *eqdata = strdup(*s); 274 | if (!eqdata) 275 | return NULL; 276 | curr->eqkid = (tst_node *) eqdata; 277 | return (void *) eqdata; 278 | } else { /* save pointer to 's' (allocated elsewhere) */ 279 | curr->eqkid = (tst_node *) *s; 280 | return (void *) *s; 281 | } 282 | } 283 | pcurr = &(curr->eqkid); 284 | } 285 | } 286 | 287 | /** tst_search(), non-recursive find of a string internary tree. 288 | * returns pointer to 's' on success, NULL otherwise. 289 | */ 290 | void *tst_search(const tst_node *p, const char *s) 291 | { 292 | const tst_node *curr = p; 293 | 294 | while (curr) { /* loop over each char in 's' */ 295 | int diff = *s - curr->key; /* calculate the difference */ 296 | if (diff == 0) { /* handle the equal case */ 297 | if (*s == 0) /* if *s = curr->key = nul-char, 's' found */ 298 | return (void *) curr->eqkid; /* return pointer to 's' */ 299 | s++; 300 | curr = curr->eqkid; 301 | } else if (diff < 0) /* handle the less than case */ 302 | curr = curr->lokid; 303 | else 304 | curr = curr->hikid; /* handle the greater than case */ 305 | } 306 | return NULL; 307 | } 308 | 309 | /** fill ptr array 'a' with strings matching prefix at node 'p'. 310 | * the 'a' array will hold pointers to stored strings with prefix 311 | * matching the string passed to tst_matching, ending in 'c', the 312 | * nchr'th char in in each matched string. 313 | */ 314 | void tst_suggest(const tst_node *p, 315 | const char c, 316 | const size_t nchr, 317 | char **a, 318 | int *n, 319 | const int max) 320 | { 321 | if (!p || *n == max) 322 | return; 323 | tst_suggest(p->lokid, c, nchr, a, n, max); 324 | if (p->key) 325 | tst_suggest(p->eqkid, c, nchr, a, n, max); 326 | else if (*(((char *) p->eqkid) + nchr - 1) == c) 327 | a[(*n)++] = (char *) p->eqkid; 328 | tst_suggest(p->hikid, c, nchr, a, n, max); 329 | } 330 | 331 | /** tst_search_prefix fills ptr array 'a' with words prefixed with 's'. 332 | * once the node containing the first prefix matching 's' is found 333 | * tst_suggest is called to travers the ternary_tree beginning 334 | * at the node filling 'a' with pointers to all words that contain 335 | * the prefix upto 'max' words updating 'n' with the number of word 336 | * in 'a'. a pointer to the first node is returned on success 337 | * NULL otherwise. 338 | */ 339 | void *tst_search_prefix(const tst_node *root, 340 | const char *s, 341 | char **a, 342 | int *n, 343 | const int max) 344 | { 345 | const tst_node *curr = root; 346 | const char *start = s; 347 | 348 | if (!*s) 349 | return NULL; 350 | 351 | /* get length of s */ 352 | for (; *start; start++) 353 | ; /* wait */ 354 | const size_t nchr = start - s; 355 | 356 | start = s; /* reset start to s */ 357 | *n = 0; /* initialize n - 0 */ 358 | 359 | /* Loop while we haven't hit a NULL node or returned */ 360 | while (curr) { 361 | int diff = *s - curr->key; /* calculate the difference */ 362 | if (diff == 0) { /* handle the equal case */ 363 | /* check if prefix number of chars reached */ 364 | if ((size_t)(s - start) == nchr - 1) { 365 | /* call tst_suggest to fill a with pointer to matching words */ 366 | tst_suggest(curr, curr->key, nchr, a, n, max); 367 | return (void *) curr; 368 | } 369 | if (*s == 0) /* no matching prefix found in tree */ 370 | return (void *) curr->eqkid; 371 | 372 | s++; 373 | curr = curr->eqkid; 374 | } else if (diff < 0) /* handle the less than case */ 375 | curr = curr->lokid; 376 | else 377 | curr = curr->hikid; /* handle the greater than case */ 378 | } 379 | return NULL; 380 | } 381 | 382 | /** tst_traverse_fn(), traverse tree calling 'fn' on each word. 383 | * prototype for 'fn' is void fn(const void *, void *). data can 384 | * be NULL if unused. 385 | */ 386 | void tst_traverse_fn(const tst_node *p, 387 | void(fn)(const void *, void *), 388 | void *data) 389 | { 390 | if (!p) 391 | return; 392 | tst_traverse_fn(p->lokid, fn, data); 393 | if (p->key) 394 | tst_traverse_fn(p->eqkid, fn, data); 395 | else 396 | fn(p, data); 397 | tst_traverse_fn(p->hikid, fn, data); 398 | } 399 | 400 | /** free the ternary search tree rooted at p, data storage internal. */ 401 | void tst_free_all(tst_node *p) 402 | { 403 | if (!p) 404 | return; 405 | tst_free_all(p->lokid); 406 | if (p->key) 407 | tst_free_all(p->eqkid); 408 | tst_free_all(p->hikid); 409 | if (!p->key) 410 | free(p->eqkid); 411 | free(p); 412 | } 413 | 414 | /** free the ternary search tree rooted at p, data storage external. */ 415 | void tst_free(tst_node *p) 416 | { 417 | if (!p) 418 | return; 419 | tst_free(p->lokid); 420 | if (p->key) 421 | tst_free(p->eqkid); 422 | tst_free(p->hikid); 423 | free(p); 424 | } 425 | 426 | /** access functions tst_get_key(), tst_get_refcnt, & tst_get_string(). 427 | * provide access to struct members through opaque pointers availale 428 | * to program. 429 | */ 430 | char tst_get_key(const tst_node *node) 431 | { 432 | return node->key; 433 | } 434 | 435 | unsigned tst_get_refcnt(const tst_node *node) 436 | { 437 | return node->refcnt; 438 | } 439 | 440 | char *tst_get_string(const tst_node *node) 441 | { 442 | if (node && !node->key) 443 | return (char *) node->eqkid; 444 | 445 | return NULL; 446 | } 447 | --------------------------------------------------------------------------------