Generally nice code: easy to read, good use of const char*
for the string arguments.
It's great that we have unit tests; we can improve them by making them self-checking:
/* return number of test failures (0 or 1) */
int test_one_away(const char *str1, const char *str2, int expected)
{
const int actual = one_way(str1, str2);
if (actual == expected)
return 0;
fprintf(stderr, "one_way \"%s\", \"%s\" expected %d but got %d\n",
str1, str2, expected, actual);
return 1;
}
int main(void)
{
return
+ test_one_away("pale", "ple", 1)
+ test_one_away("pales", "pale", 1)
+ test_one_away("pale", "bale", 1)
+ test_one_away("pale", "bake", 0);
}
We should add some more tests. When I start writing tests (usually before the production code) I usually write the very simplest tests first, probably passing NULL
to exercise the error recovery. I'm going to assume that the code won't be testing for null pointers, but would certainly start with the next simplest case: are two empty strings within one change:
+ test_one_away("" "", 1)
Then compare one- and two-character strings against the empty string, and for strings that differ by one or two deletions at the beginning, middle and end. A good guideline for testing functions that return a boolean value is to identify the boundaries where the result should change between false and true (e.g. remove one character from the front ⇒ true; remove two characters from front ⇒ false) and write one test for each side of that transition.
Here's a test that exposes a bug in the code:
+ test_one_away("pale", "ale", 1)
This is because we don't accept deletion of the first character:
if (flag && i) --i;
The fix is to remove the second part of the condition (remember, unsigned overflow is well-defined, and will exactly match the ++i
in the loop increment):
if (flag) --i;
I'm not sure why flag
needs to be a size_t
; a simple int
should be sufficient. If/when we have access to a C99 compiler (which should be soon; it's around 20 years old now), we could include <stdbool.h>
and make it a bool
. It also needs a better name; I had to look to the call site to understand what it's for (it seems that a true value means that we're looking for a deletion rather than a replacement).
The conditions in the wrapper function could be expressed more clearly with a single switch
on the difference in length:
/* check if str1 can be obtained from string2 adding, deleting, removing
* at last one char */
bool one_way(const char *str1, const char *str2)
{
size_t len1 = strlen(str1);
size_t len2 = strlen(str2);
switch (len2 - len1) {
case (size_t)-1: return helper_one_way(str2, str1, len2, true);
case 0: return helper_one_way(str1, str2, len1, false);
case 1: return helper_one_way(str1, str2, len2, true);
default: return false;
}
}
We don't need to pass the length to helper_one_way
, because it can simply stop when it reaches the terminating null char:
for (i = 0, j = 0; str1[i]; ++i, ++j) {
Given that we're iterating over strings, it's more idiomatic to use a char pointer than to repeatedly index into the string (though a good compiler ought to generate the same code):
static bool helper_one_way(const char *a, const char *b,
bool allow_deletion)
{
size_t nchars = 0;
while (*a) {
if (*a++ != *b++) {
if (++nchars > 1) return false;
if (allow_deletion) --b;
}
}
return true;
}
Finally: the name - should one_way
be spelt one_away
?
Modified code
Applying the above suggestions, we get:
#include <stdbool.h>
#include <string.h>
static bool helper_one_away(const char *a, const char *b,
bool allow_deletion)
{
size_t nchars = 0;
while (*a) {
if (*a++ != *b++) {
if (++nchars > 1) return false;
if (allow_deletion) --b;
}
}
return true;
}
/* Return true if a can be obtained from string2 by adding,
deleting, or removing at most one character */
bool one_away(const char *a, const char *b)
{
switch (strlen(a) - strlen(b)) {
case (size_t)-1: return helper_one_away(b, a, true);
case 0: return helper_one_away(a, b, false);
case 1: return helper_one_away(a, b, true);
default: return false;
}
}
/* Test code */
#include <stdio.h>
/* return number of test failures (0 or 1) */
static int test_one_away(const char *a, const char *b,
bool expected)
{
const int actual = one_away(a, b);
if (actual == expected)
return 0;
fprintf(stderr, "one_away \"%s\", \"%s\" expected %d but got %d\n",
a, b, expected, actual);
return 1;
}
int main(void)
{
return
+ test_one_away("", "", true)
+ test_one_away("", "a", true)
+ test_one_away("pale", "", false)
+ test_one_away("pale", "le", false)
+ test_one_away("pale", "ale", true)
+ test_one_away("pale", "pale", true)
+ test_one_away("pale", "pal", true)
+ test_one_away("pale", "pa", false)
+ test_one_away("pale", "ple", true)
+ test_one_away("ple", "pale", true)
+ test_one_away("pales", "pale", true)
+ test_one_away("pale", "bale", true)
+ test_one_away("pale", "bake", false);
}
adding, deleting, removing
what is the difference between the latter two items? \$\endgroup\$