master
Ondřej Hruška 4 years ago
commit 50a26664e3
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 7
      .gitignore
  2. 16
      CMakeLists.txt
  3. 19
      LICENSE.txt
  4. 9
      Makefile
  5. 169
      prefix_match.c
  6. 77
      prefix_match.h
  7. 159
      test/main.c
  8. 22
      test/test_framework.c
  9. 100
      test/test_framework.h

7
.gitignore vendored

@ -0,0 +1,7 @@
cmake-build-debug/
CMakeCache.txt
*.o
*.out
*~
.idea/

@ -0,0 +1,16 @@
# Unit tests project
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 11)
SET(CMAKE_BUILD_TYPE Debug)
project(test)
set(SOURCES prefix_match.c test/main.c test/test_framework.c)
add_executable(test ${SOURCES})
target_include_directories(test
PRIVATE "." "test"
)

@ -0,0 +1,19 @@
Copyright (c) 2020 Ondřej Hruška
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,9 @@
.PHONY: build run
build: a.out
a.out:
cc prefix_match.c test/main.c test/test_framework.c -I. -Itest -Wall -Wextra -pedantic -Og
run: a.out
./a.out

@ -0,0 +1,169 @@
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#include "prefix_match.h"
int prefix_match(const char *value, const char **options, int flags) {
flags &= ~PREFIXMATCH_MULTI_PARTIAL; // this doesn't make sense here
bool case_sensitive = PREFIXMATCH_CASE_SENSITIVE == (flags & PREFIXMATCH_CASE_SENSITIVE);
bool can_abbrev = 0 == (flags & PREFIXMATCH_NOABBREV);
int (*cmpfn) (const char *, const char *) = case_sensitive ? strcmp : strcasecmp;
int (*ncmpfn) (const char *, const char *, size_t) = case_sensitive ? strncmp : strncasecmp;
if (!value || !options) return -1;
size_t input_len = strlen(value);
const char *option = NULL;
int counter = 0;
int result = -1;
while (NULL != (option = options[counter])) {
if (cmpfn(option, value) == 0) {
return counter; // full exact match
} else {
// Test for partial match
if (can_abbrev && ncmpfn(value, option, input_len) == 0) {
if (result == -1) {
result = counter; // first partial match
} else {
// ambiguous match
return -1;
}
}
}
counter++;
}
return result;
}
size_t pm_word_len(const char *word, const char *delims) {
char d = 0;
const char *dp = delims;
size_t word_len = 0;
while ('\0' != (d = *dp++)) {
char *end = strchr(word, d);
if (NULL == end) continue;
size_t len = end - word;
if (!word_len || len < word_len) {
word_len = len;
}
}
if (!word_len) {
word_len = strlen(word);
}
return word_len;
}
size_t pm_count_words(const char *sentence, const char *delims) {
char c;
size_t n = 0;
bool in_word = false;
if (!sentence || !delims) return 0;
while (0 != (c = *sentence++)) {
bool is_delim = NULL != strchr(delims, c);
if (is_delim && in_word) {
in_word = false;
} else if (!in_word && !is_delim) {
n++;
in_word = true;
}
}
return n;
}
int pm_multipart_test(const char *a, const char* b, const char *delims, int flags) {
bool case_sensitive = PREFIXMATCH_CASE_SENSITIVE == (flags & PREFIXMATCH_CASE_SENSITIVE);
bool can_abbrev = 0 == (flags & PREFIXMATCH_NOABBREV);
int (*ncmpfn) (const char *, const char *, size_t) = case_sensitive ? strncmp : strncasecmp;
// lazy shortcut first...
if ((case_sensitive && 0 == strcmp(a, b)) || (!case_sensitive && 0 == strcasecmp(a, b))) {
return 1; // full match
}
const char *word_a = a;
const char *word_b = b;
size_t word_a_len = 0;
size_t word_b_len = 0;
while (1) {
word_a += word_a_len;
word_b += word_b_len;
// advance past leading delims, if any
while (*word_a != '\0' && NULL != strchr(delims, *word_a)) word_a++;
while (*word_b != '\0' && NULL != strchr(delims, *word_b)) word_b++;
// test for terminator
if (*word_a == '\0' && *word_b == '\0') {
// both ended at the same number of words
return 1; // full match
}
if (*word_a == '\0' || *word_b == '\0') {
// sentences ended at different length
if (0 != (flags & PREFIXMATCH_MULTI_PARTIAL) && *word_b != '\0') { // word prefix match (a is a prefix of b)
return 2; // partial word match
} else {
return 0; // no match
}
}
// find end of the words
word_a_len = pm_word_len(word_a, delims);
word_b_len = pm_word_len(word_b, delims);
if (word_a_len > word_b_len || (!can_abbrev && word_a_len != word_b_len)) {
return 0; // no match
}
int cmp = ncmpfn(word_a, word_b, word_a_len);
if (0 != cmp) { // words differ
return 0; // no match
}
}
}
int prefix_multipart_match(const char *value, const char **options, const char* delims, int flags) {
bool multi_partial = 0 != (flags & PREFIXMATCH_MULTI_PARTIAL);
flags &= ~PREFIXMATCH_MULTI_PARTIAL; // turn it off for passing the to test fn
bool can_abbrev = 0 == (flags & PREFIXMATCH_NOABBREV);
if (!value || !options) return -1;
const char *option = NULL;
int counter = 0;
int result = -1;
int result_partial = -1;
int result_partial_nwords = 0;
while (NULL != (option = options[counter])) {
if (pm_multipart_test(value, option, delims, flags | PREFIXMATCH_NOABBREV)) {
return counter; // full exact match
} else if (can_abbrev) {
// Test for partial match
if (pm_multipart_test(value, option, delims, flags)) {
if (result == -1) {
result = counter; // first partial match in all words
} else {
return -1;
}
} else if (multi_partial && 2 == pm_multipart_test(value, option, delims, flags | PREFIXMATCH_MULTI_PARTIAL)) {
int nwords = pm_count_words(option, delims);
if (result_partial == -1 || result_partial_nwords < nwords) {
result_partial = counter; // first partial match
result_partial_nwords = nwords;
} else {
result_partial = -2;
}
}
}
counter++;
}
if (result != -1) {
return result;
}
if (result_partial >= 0) {
return result_partial;
}
return -1;
}

@ -0,0 +1,77 @@
/**
* Prefix Match
*
* Match input value to a list of options, allowing non-ambiguous abbreviation and partial matching.
* This library was designed for command recognition in interactive consoles and command interfaces.
*
* Created on 2020/06/09 by Ondřej Hruška
*/
#ifndef _PREFIX_MATCH_H
#define _PREFIX_MATCH_H
#include <stdbool.h>
/** Use case-sensitive matching */
#define PREFIXMATCH_CASE_SENSITIVE 1
/** Forbid abbreviations */
#define PREFIXMATCH_NOABBREV 2
/** Allow matching fewer words, if unambiguous */
#define PREFIXMATCH_MULTI_PARTIAL 4
/**
* Recognize (optionally abbreviated) input
*
* @param[in] value - tested value
* @param[in] options - options to match against
* @param[in] flags - matching options (bitmask) - accepts PREFIXMATCH_CASE_SENSITIVE and PREFIXMATCH_NOABBREV
* @return index of the matched option, -1 on mismatch or ambiguous match
*/
int prefix_match(const char *value, const char **options, int flags);
/**
* Recognize input consisting of one or more (optionally abbreviated) words
*
* @param[in] value - tested value
* @param[in] options - options to match against, multi-word options separated by the listed delimiters
* @param[in] delims - string with a list of possible delimiters (like for strtok)
* @param[in] flags - matching options (bitmask) - accepts all options
* @return index of the matched option, -1 on mismatch or ambiguous match
*/
int prefix_multipart_match(const char *value, const char **options, const char* delims, int flags);
// useful internal functions exported for possible re-use
/**
* Test if two word sentences match, with individual words optionally allowed to be abbreviated.
*
* @internal
* @param[in] a - tested (optionally abbreviated) sentence
* @param[in] b - full sentence
* @param[in] delims - list of possible delimiters, same may be used for both sentences
* @param[in] flags - matching options (bitmask) - accepts all options
* @return 1-match; 0-no match; 2-partial (some words) match, if the PREFIXMATCH_MULTI_PARTIAL flag is set
*/
int pm_multipart_test(const char *a, const char* b, const char *delims, int flags);
/**
* Count words in a "sentence", delimited by any of the given set of delimiters.
*
* @internal
* @param[in] sentence - one or multi-word string
* @param[in] delims - delimiters accepted
* @return number of words
*/
size_t pm_count_words(const char *sentence, const char *delims);
/**
* Measure word length
*
* @internal
* @param[in] word - start of a word that ends with either one of the delimiters, or a null byte.
* @param[in] delims - delimiters accepted
* @return word length
*/
size_t pm_word_len(const char *word, const char *delims);
#endif //_PREFIX_MATCH_H

@ -0,0 +1,159 @@
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include "prefix_match.h"
#include "test_framework.h"
// --- test cases ---
bool test_prefix_match() {
const char *options[] = {
"ahoj",//0
"ahojahoj",//1
"ble",//2
"citron",//3
"foo",//4
"foo bar",//5
"foo baz",//6
"foo bcz",//7
"eps",//8
"eps set",//9
"eps set config",//10
"eps get config",//11
"eps hk2",//12
"eps hk2 vi",//13
"eps hk2 out",//14
"delete from table users",//15
"delete,from;,,,,table sessions",//16
NULL
};
check_eq(prefix_match("", options, 0), -1);
check_eq(prefix_match("b", options, 0), 2);
check_eq(prefix_match("c", options, 0), 3);
check_eq(prefix_match("citron", options, 0), 3);
check_eq(prefix_match("foo", options, 0), 4);
check_eq(prefix_match("foo ", options, 0), -1);
check_eq(prefix_match("foo bc", options, 0), 7);
check_eq(prefix_match("foo bcz", options, 0), 7);
check_eq(prefix_match("fOO bcz", options, PREFIXMATCH_CASE_SENSITIVE), -1); // CS
check_eq(prefix_match("fOO bcz", options, 0), 7); // CI
check_eq(prefix_match("ahoj", options, 0), 0);
check_eq(prefix_match("ahoja", options, 0), 1);
return true;
}
bool test_multipart_test() {
check_eq(pm_multipart_test("ahoj", "ahoj", " ", 0), 1);
check_eq(pm_multipart_test("ah", "ahoj", " ", 0), 1);
check_eq(pm_multipart_test("xxxx", "ahoj", " ", 0), 0);
check_eq(pm_multipart_test("", "ahoj", " ", 0), 0);
check_eq(pm_multipart_test("", "", " ", 0), 1);
check_eq(pm_multipart_test("multi part", "multi part", " ", 0), 1);
check_eq(pm_multipart_test("multi", "multi part", " ", 0), 0);
check_eq(pm_multipart_test("multi part", "multi", " ", 0), 0);
check_eq(pm_multipart_test("multi part", "multi dog", " ", 0), 0);
check_eq(pm_multipart_test("multi part", "multi part", " ", 0), 1);
check_eq(pm_multipart_test(" multi part ", "multi part", " ", 0), 1);
check_eq(pm_multipart_test("multi ,; part", "multi,part", ",; ", 0), 1);
check_eq(pm_multipart_test("m p", "multi part", " ", 0), 1);
check_eq(pm_multipart_test("mu pa", "multi part", " ", 0), 1);
check_eq(pm_multipart_test("mu pp", "multi part", " ", 0), 0);
check_eq(pm_multipart_test("m ", "multi part", " ", PREFIXMATCH_MULTI_PARTIAL), 2);
check_eq(pm_multipart_test("v l s h", "very long sentence here", " ", 0), 1);
check_eq(pm_multipart_test("v l s h", "very long sentence here too", " ", 0), 0);
check_eq(pm_multipart_test("v l", "very long sentence here too", " ", 0), 0);
check_eq(pm_multipart_test("v l", "very long sentence here too", " ", PREFIXMATCH_MULTI_PARTIAL), 2);
check_eq(pm_multipart_test("v l s h", "very long sentence here", " ", PREFIXMATCH_MULTI_PARTIAL | PREFIXMATCH_CASE_SENSITIVE), 1);
check_eq(pm_multipart_test("v l s h", "very long sentence here too", " ", PREFIXMATCH_MULTI_PARTIAL), 2);
return true;
}
bool test_multipart_match() {
const char *options[] = {
"ahoj",//0
"foo",//1
"foo bar",//2
"foo baz",//3
"foo bcz",//4
"eps", // 5
"eps set",//6
"eps set config",//7
"eps get config",//8
"eps hk2",//9
"eps hk2 vi", //10
"eps hk2 out",//11
"delete from table users",//12
"delete,from;,,,,table sessions",//13
NULL
};
check_eq(prefix_multipart_match("", options, " ", 0), -1);
check_eq(prefix_multipart_match("x", options, " ", 0), -1);
check_eq(prefix_multipart_match("ahoj", options, " ", 0), 0);
check_eq(prefix_multipart_match("a", options, " ", 0), 0);
check_eq(prefix_multipart_match("ah", options, " ", 0), 0);
check_eq(prefix_multipart_match("foo", options, " ", 0), 1);
check_eq(prefix_multipart_match("f", options, " ", 0), 1);
check_eq(prefix_multipart_match("f b", options, " ", 0), -1);
check_eq(prefix_multipart_match("f b", options, " ", PREFIXMATCH_MULTI_PARTIAL), -1);
check_eq(prefix_multipart_match("f bc", options, " ", 0), 4);
check_eq(prefix_multipart_match("e", options, " ", 0), 5);
check_eq(prefix_multipart_match("eps", options, " ", 0), 5);
check_eq(prefix_multipart_match("eps banana", options, " ", 0), -1);
check_eq(prefix_multipart_match("eps banana", options, " ", PREFIXMATCH_MULTI_PARTIAL), -1);
check_eq(prefix_multipart_match("eps set", options, " ", 0), 6);
check_eq(prefix_multipart_match("e s", options, " ", 0), 6);
check_eq(prefix_multipart_match("e set", options, " ", 0), 6);
check_eq(prefix_multipart_match("eps s", options, " ", 0), 6);
check_eq(prefix_multipart_match("eps s", options, " ", 0), 6);
check_eq(prefix_multipart_match("e s c", options, " ", 0), 7);
check_eq(prefix_multipart_match("e g c", options, " ", 0), 8);
// there is only one get command
check_eq(prefix_multipart_match("eps get", options, " ", 0), -1);
check_eq(prefix_multipart_match("eps get", options, " ", PREFIXMATCH_MULTI_PARTIAL), 8);
check_eq(prefix_multipart_match("e g", options, " ", 0), -1);
check_eq(prefix_multipart_match("e g", options, " ", PREFIXMATCH_MULTI_PARTIAL), 8);
check_eq(prefix_multipart_match("epx get", options, " ", 0), -1);
check_eq(prefix_multipart_match("epx get", options, " ", PREFIXMATCH_MULTI_PARTIAL), -1);
check_eq(prefix_multipart_match("d f t u", options, " ", 0), 12);
check_eq(prefix_multipart_match("d f t s", options, ",; ", 0), 13);
return true;
}
// --- test launcher ---
static struct Test tests[] = {
{"prefix_match", test_prefix_match},
{"prefix_multipart_test", test_multipart_test},
{"prefix_multipart_match", test_multipart_match},
{NULL, NULL}
};
int main() {
run_tests(tests, "main");
return 0;
}

@ -0,0 +1,22 @@
#include <stdio.h>
#include "test_framework.h"
void run_tests(struct Test *tests, const char *module_name) {
printf("Running tests module \"%s\"...\n\n", module_name);
struct Test *t = &tests[0];
int passed = 0, failed = 0;
do {
printf("Running test \"%s\"...\n", t->name);
if (t->func()) {
printf("Test %s \x1b[32mPASSED\x1b[m\n", t->name);
passed++;
} else {
printf("Test %s \x1b[31mFAILED\x1b[m\n", t->name);
failed++;
}
} while((++t)->name != NULL);
printf("\nTests module \"%s\" done. %d passed, %d failed.\n\n", module_name, passed, failed);
}

@ -0,0 +1,100 @@
/**
* shared defines and utils for tests
*
* Created on 2020/05/12.
*/
#ifndef TESTS_TEST_FRAMEWORK_H
#define TESTS_TEST_FRAMEWORK_H
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <math.h>
#define XSTR(s) STR(s)
#define STR(s) #s
typedef bool(*testfn_t)(void);
struct Test {
const char *name;
testfn_t func;
};
void run_tests(struct Test *tests, const char *module_name);
// _good must be an array, not a pointer
#define check_array(_var, _good) \
do { \
for (int i = 0; i < (int)sizeof(_good); i++) { \
if (_var[i] != _good[i]) { \
printf("%s:%d \x1b[31mAssert failed\x1b[m: arrays differ\n\x1b[37;1m n# exp act\x1b[m\n", __func__, __LINE__); \
for (i = 0; i < (int)sizeof(_good); i++) { \
if (_var[i] == _good[i]) { \
printf(" %2d - 0x%02x -\n", i, _var[i]); \
} else { \
printf(" %2d \x1b[32m0x%02x \x1b[31m0x%02x\x1b[m\n", i, _good[i], _var[i]); \
} \
} \
return false; \
} \
} \
} while(0)
#define check(_cond) \
do { \
if (!(_cond)) { \
printf("%s:%d \x1b[31mAssert failed\x1b[m: %s\n", __func__, __LINE__, STR(_cond)); \
return false; \
} \
} while(0)
#define check_eq(_var, _good) \
do { \
if ((_var) != (_good)) { \
printf("%s:%d \x1b[31mAssert failed\x1b[m: %s == %s\n", __func__, __LINE__, STR(_var), STR(_good)); \
printf(" \x1b[31m%d (0x%02x)\x1b[m != \x1b[32m%d (0x%02x)\x1b[m\n", _var, _var, _good, _good); \
return false; \
} \
} while(0)
#define check_eq_ul(_var, _good) \
do { \
if ((_var) != (_good)) { \
printf("%s:%d \x1b[31mAssert failed\x1b[m: %s == %s\n", __func__, __LINE__, STR(_var), STR(_good)); \
printf(" \x1b[31m%ld (0x%02lx)\x1b[m != \x1b[32m%ld (0x%02lx)\x1b[m\n", _var, _var, _good, _good); \
return false; \
} \
} while(0)
#define check_eq_l(_var, _good) \
do { \
if ((_var) != (_good)) { \
printf("%s:%d \x1b[31mAssert failed\x1b[m: %s == %s\n", __func__, __LINE__, STR(_var), STR(_good)); \
printf(" \x1b[31m%ld\x1b[m != \x1b[32m%ld\x1b[m\n", _var, _good); \
return false; \
} \
} while(0)
#define check_eq_f(_var, _good) \
do { \
if ((_var) != (_good)) { \
printf("%s:%d \x1b[31mAssert failed\x1b[m: %s == %s\n", __func__, __LINE__, STR(_var), STR(_good)); \
printf(" \x1b[31m%f\x1b[m != \x1b[32m%f\x1b[m\n", _var, _good); \
return false; \
} \
} while(0)
#define check_eq_f_safe(_var, _good, thr) \
do { \
if (fabs((double)((_var) - (_good))) > thr) { \
printf("%s:%d \x1b[31mAssert failed\x1b[m: %s == %s\n", __func__, __LINE__, STR(_var), STR(_good)); \
printf(" \x1b[31m%f\x1b[m != \x1b[32m%f\x1b[m\n", _var, _good); \
return false; \
} \
} while(0)
#endif //TESTS_TEST_FRAMEWORK_H
Loading…
Cancel
Save