From db9351b48de034e86ca3a15b48a311b910e79350 Mon Sep 17 00:00:00 2001 From: ajborla Date: Mon, 6 May 2024 07:32:26 +1000 Subject: [PATCH] feature(reverse-string): add reverse-string practice exercise --- config.json | 8 + .../reverse-string/.docs/instructions.md | 7 + .../practice/reverse-string/.meta/config.json | 11 ++ .../practice/reverse-string/.meta/example.prg | 17 ++ .../practice/reverse-string/.meta/tests.toml | 28 ++++ exercises/practice/reverse-string/PRGUNIT.prg | 153 ++++++++++++++++++ .../reverse-string/reverse_string.prg | 8 + .../reverse-string/reverse_string_test.prg | 48 ++++++ exercises/practice/reverse-string/utils.prg | 86 ++++++++++ 9 files changed, 366 insertions(+) create mode 100644 exercises/practice/reverse-string/.docs/instructions.md create mode 100644 exercises/practice/reverse-string/.meta/config.json create mode 100644 exercises/practice/reverse-string/.meta/example.prg create mode 100644 exercises/practice/reverse-string/.meta/tests.toml create mode 100644 exercises/practice/reverse-string/PRGUNIT.prg create mode 100644 exercises/practice/reverse-string/reverse_string.prg create mode 100644 exercises/practice/reverse-string/reverse_string_test.prg create mode 100644 exercises/practice/reverse-string/utils.prg diff --git a/config.json b/config.json index 23327e4..9a77562 100644 --- a/config.json +++ b/config.json @@ -137,6 +137,14 @@ "practices": [], "prerequisites": [], "difficulty": 2 + }, + { + "slug": "reverse-string", + "name": "Reverse String", + "uuid": "5e273619-6a13-443f-8734-8ff1e9963f2e", + "practices": [], + "prerequisites": [], + "difficulty": 2 } ] }, diff --git a/exercises/practice/reverse-string/.docs/instructions.md b/exercises/practice/reverse-string/.docs/instructions.md new file mode 100644 index 0000000..039ee33 --- /dev/null +++ b/exercises/practice/reverse-string/.docs/instructions.md @@ -0,0 +1,7 @@ +# Instructions + +Reverse a string + +For example: +input: "cool" +output: "looc" diff --git a/exercises/practice/reverse-string/.meta/config.json b/exercises/practice/reverse-string/.meta/config.json new file mode 100644 index 0000000..2802886 --- /dev/null +++ b/exercises/practice/reverse-string/.meta/config.json @@ -0,0 +1,11 @@ +{ + "authors": ["ajborla"], + "files": { + "solution": ["reverse_string.prg"], + "test": ["reverse_string_test.prg"], + "example": [".meta/example.prg"] + }, + "blurb": "Reverse a string.", + "source": "Introductory challenge to reverse an input string", + "source_url": "https://medium.freecodecamp.org/how-to-reverse-a-string-in-javascript-in-3-different-ways-75e4763c68cb" +} diff --git a/exercises/practice/reverse-string/.meta/example.prg b/exercises/practice/reverse-string/.meta/example.prg new file mode 100644 index 0000000..b1feeac --- /dev/null +++ b/exercises/practice/reverse-string/.meta/example.prg @@ -0,0 +1,17 @@ +* ---------------------------------------------------------------------------- +* exercism.org +* Harbour Track Exercise: reverse-string +* Contributed: Anthony J. Borla (ajborla@bigpond.com) +* ---------------------------------------------------------------------------- + +function ReverseString(string) + local i, rstring := "" + * Reject missing, or wrong number of arguments + if PCOUNT() <> 1 ; return NIL ; endif + + * Build return string by traversing input string backwards (right-to-left) + for i := LEN(string) to 1 step -1 + rstring := rstring + SUBSTR(string, i, 1) + next +return rstring + diff --git a/exercises/practice/reverse-string/.meta/tests.toml b/exercises/practice/reverse-string/.meta/tests.toml new file mode 100644 index 0000000..0b04c4c --- /dev/null +++ b/exercises/practice/reverse-string/.meta/tests.toml @@ -0,0 +1,28 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[c3b7d806-dced-49ee-8543-933fd1719b1c] +description = "an empty string" + +[01ebf55b-bebb-414e-9dec-06f7bb0bee3c] +description = "a word" + +[0f7c07e4-efd1-4aaa-a07a-90b49ce0b746] +description = "a capitalized word" + +[71854b9c-f200-4469-9f5c-1e8e5eff5614] +description = "a sentence with punctuation" + +[1f8ed2f3-56f3-459b-8f3e-6d8d654a1f6c] +description = "a palindrome" + +[b9e7dec1-c6df-40bd-9fa3-cd7ded010c4c] +description = "an even-sized word" diff --git a/exercises/practice/reverse-string/PRGUNIT.prg b/exercises/practice/reverse-string/PRGUNIT.prg new file mode 100644 index 0000000..99d8c4e --- /dev/null +++ b/exercises/practice/reverse-string/PRGUNIT.prg @@ -0,0 +1,153 @@ +* ---------------------------------------------------------------------------- +* Harbour Unit Test Worker +* Anthony J. Borla (ajborla@bigpond.com) +* ---------------------------------------------------------------------------- + +#ifndef UTILS_PRG + #include "utils.prg" +#endif + +procedure MakeTestDatabaseStructure(dbfName) + * Creation overwites any existing database + create &dbfName + + * Each record describes the structure of a FIELD + append blank + replace Field_name with "NAME", Field_type with "C",; + Field_Len with 80, Field_dec with 0 + append blank + replace Field_name with "CMPOP", Field_type with "C",; + Field_Len with 2, Field_dec with 0 + append blank + replace Field_name with "EXPVALUE", Field_type with "C",; + Field_Len with 80, Field_dec with 0 + append blank + replace Field_name with "CMDSTR", Field_type with "C",; + Field_Len with 80, Field_dec with 0 + + * Ensure data written to disk + close &dbfName +return + +procedure MakeTestDatabase(dbfName) + local dbfStructure := dbfName + "_STRUCTURE" + + * Build test database from database structure file + do MakeTestDatabaseStructure with dbfStructure + create &dbfName from &dbfStructure + + * Ensure database structure file is removed + dbfStructure := dbfStructure + ".dbf" + erase &dbfStructure +return + +procedure AddTestDatabase(dbfName, testName, cmpOp, expValue, cmdStr) + * Load a test data record into tests database (note use of 'Wrap' to + * preserve spaces in expected value string) + use &dbfName + append blank + replace &dbfName->NAME with testName + replace &dbfName->CMPOP with cmpOp + replace &dbfName->EXPVALUE with Wrap(expValue) + replace &dbfName->CMDSTR with cmdStr + close &dbfName +return + +function RunTests(dbfName, keepTestDBF, outputJSON) + local testName, cmpOp, expValue, cmdStr, retValue, testExpr + local success := .T. + + use &dbfName + + * Determine, and print, number of tests (required for TAP) + if outputJSON == NIL .OR. !outputJSON + ? "1.." + LTRIM(STR(LASTREC())) + endif + + * Execute unit tests + do while !EOF() + * Extract test data (note use of 'Unwrap' to extract space-preserved + * expected value string) + testName := ALLTRIM(&dbfName->NAME) + cmpOp := &dbfName->CMPOP + expValue := Unwrap(ALLTRIM(&dbfName->EXPVALUE)) + cmdStr := ALLTRIM(&dbfName->CMDSTR) + + * Execute test, and build test expression + retValue := TypeToS(&cmdStr) + testExpr := '"' + retValue + '" ' + cmpOp + ' "' + expValue + '"' + + * If the parameter flag, outputJSON, is omitted, or set to .F., then + * emit test report in TAP format + if outputJSON == NIL .OR. !outputJSON + * Report test outcome - TAP + if &testExpr + ? "OK " + LTRIM(STR(RECNO())) + " - " + testName + else + * Single test failure signals failure of whole suite + success := .F. + ? "FAIL " + LTRIM(STR(RECNO())) + " - " + testName + endif + else + * Report test outcome - JSON + ? "JSON" + endif + + * ... next test + skip + enddo + + close &dbfName + + * If the parameter flag, keepTestDBF, is omitted, or set to .F., then + * remove the tests database + if keepTestDBF == NIL .OR. !keepTestDBF + dbfName := dbfName + ".dbf" + erase &dbfName + endif + +return success + +function TypeToS(value) + * Use VALTYPE() instead of TYPE() to check type + local typeValue := VALTYPE(value) + + switch typeValue + * Array type (assume 1D array of non-aggregate elements), + * returns the concatenation of elements as a string + case "A" ; return ArrToS(value) + + * Character type returned untouched + case "C" ; return value + + * Date as "yyyymmdd" + case "D" ; return DTOS(value) + + * Logical as literal string representation of self + case "L" ; return IIF(value, ".T.", ".F.") + + * String-converted numerics are right-justified, so ensure are + * returned trimmed + case "N" ; return ALLTRIM(STR(value)) + + * Support use of NIL return type (usually to indicate error) + case "U" ; return "NIL" + endswitch + +* Ignore the remaining types, just return NIL (likely runtime error) +return NIL + +* Utilities to preserve leading and trailing spaces in strings as they +* are stored into, and extracted from, database fields +function Wrap(string) ; return WrapString(string, .F., "[", "]") +function Unwrap(string) ; return WrapString(string, .T.) + +function WrapString(string, doUnwrap, wrapStart, wrapEnd) + local uws + if doUnwrap + uws := SUBSTR(SUBSTR(string, 2), 1, LEN(string) - 2) + else + uws := wrapStart + string + wrapEnd + endif +return uws + diff --git a/exercises/practice/reverse-string/reverse_string.prg b/exercises/practice/reverse-string/reverse_string.prg new file mode 100644 index 0000000..54d3c98 --- /dev/null +++ b/exercises/practice/reverse-string/reverse_string.prg @@ -0,0 +1,8 @@ +* ---------------------------------------------------------------------------- +* exercism.org +* Harbour Track Exercise: reverse-string +* ---------------------------------------------------------------------------- + +function ReverseString(string) +return "" + diff --git a/exercises/practice/reverse-string/reverse_string_test.prg b/exercises/practice/reverse-string/reverse_string_test.prg new file mode 100644 index 0000000..fadb9ff --- /dev/null +++ b/exercises/practice/reverse-string/reverse_string_test.prg @@ -0,0 +1,48 @@ +* ---------------------------------------------------------------------------- +* Harbour Unit Test Runner [reverse_string.prg] +* ---------------------------------------------------------------------------- + +* Variable declarations +memvar TESTS, SUCCESS + +* Test database name +TESTS := IIF(PCOUNT() > 0, hb_PValue(1), "TESTS") + +* Create tests database +do MakeTestDatabase with TESTS + +* Add test data into tests database. Each test case stub should be altered +* to follow this format: +* +* do AddTestDatabase with TESTS, "say Hi!", "==", "Hello, World!", "HelloWorld()" +* do AddTestDatabase with TESTS, "add 5 and 3", "==", "8", "Add_5_And_3(5, 3)" +* +* Note: +* 1st field is the test description (already supplied) +* 2nd field is comparator operator, usually "==" +* 3rd field is the function return result, always written as a string +* 4th field is the function (optionally with arguments), always written as a string +* + +* Add test data into tests database +do AddTestDatabase with TESTS, "Empty string", "==", "", "ReverseString('')" +do AddTestDatabase with TESTS, "A word", "==", "tobor", "ReverseString('robot')" +do AddTestDatabase with TESTS, "A capitalised word", "==", "nemaR", "ReverseString('Ramen')" +do AddTestDatabase with TESTS, "A sentence with punctuation", "==", "!yrgnuh m'I", "ReverseString('I'+CHR(39)+'m hungry!')" +do AddTestDatabase with TESTS, "A palindrome", "==", "racecar", "ReverseString('racecar')" +do AddTestDatabase with TESTS, "An even-sized word", "==", "reward", "ReverseString('drawer')" + +* Execute unit tests. Arguments: +* - Tests database name +* - Database retention flag (.T. to not delete test database on test end) +* - JSON output flag (.T. to emit test results in JSON format [default is TAP]) +SUCCESS := RunTests(TESTS, SToBool(hb_PValue(2)), SToBool(hb_PValue(3))) + +* Return success status to OS +ERRORLEVEL(IIF(SUCCESS, 0, 1)) + +* Code under test (CUT) +#include "reverse_string.prg" + +* Unit Test Framework +#include "PRGUNIT.prg" diff --git a/exercises/practice/reverse-string/utils.prg b/exercises/practice/reverse-string/utils.prg new file mode 100644 index 0000000..a6275f9 --- /dev/null +++ b/exercises/practice/reverse-string/utils.prg @@ -0,0 +1,86 @@ +* ---------------------------------------------------------------------------- +* Harbour Miscellaneous Utilities +* Anthony J. Borla (ajborla@bigpond.com) +* ---------------------------------------------------------------------------- + +#define UTILS_PRG + +* +* Given a string, charSet, interpreted as a set of individual characters, +* and a string, string, returns a copy of string with all occurrences of +* the characters in charSet, removed. +* +function RemoveCharSet(charSet, string) + local i, clen := LEN(charSet) + for i := 1 to clen + string := STRTRAN(string, SUBSTR(charSet, i, 1)) + next +return string + +* +* Given a string, and a separator string (usually a single character) +* returns an array of separator-split tokens, or the original string +* if separation not possible. +* +function SToArr(string, separator) + local array := {}, i, element + + * Return untouched string if no separator, or it is not in string + if PCOUNT() < 2 .OR. separator == NIL ; return string ; endif + i := AT(separator, string) ; if i == 0 ; return string ; endif + + * Parse the string, extracting each element, and adding to array + do while i <> 0 + element := LEFT(string, i - 1) + if !EMPTY(element) ; AADD(array, element) ; endif + string := SUBSTR(string, i + 1) + i := AT(separator, string) + enddo + + * Handle last element, and return array + if !EMPTY(string) ; AADD(array, string) ; endif +return array + +* +* Given an array whose elements are non-aggregate types, returns a +* string of those elements separated by separator. If a string +* cannot be built, an empty string is returned. +* +function ArrToS(array, separator) + local i, element, string := "", arrlen := LEN(array) + if PCOUNT() < 2 .OR. separator == NIL ; separator := "" ; endif + if arrlen < 1 ; return "" ; endif + for i := 1 to arrlen + element := IIF(VALTYPE(array[i]) <> "C", ALLTRIM(STR(array[i])), array[i]) + string += element + separator + next +return ; + IIF(EMPTY(separator), string, SUBSTR(string, 1, RAT(separator, string) - 1)) + +* +* Given a string, returns the Boolean status indicating whether +* it is convertible to an integer. Non-numeric and floating +* point values will both return .F. +* +function IsINTString(s) + local slen, i + if PCOUNT() <> 1 .OR. VALTYPE(s) <> "C" ; return .F. ; endif + slen := LEN(s) + if AT(".", s) <> 0 .OR. slen < 1 ; return .F. ; endif + if VAL(s) <> 0 ; return .T. ; endif + for i := 1 to slen + if !(SUBSTR(s, i, 1) $ "0123456789") ; return .F. ; endif + next +return .T. + +* +* Given a string, returns the Boolean value represented. +* +function SToBool(s) +return ; + IIF(VALTYPE(s) <> "C", NIL, ; + IIF(UPPER(s) == ".T.", .T., ; + IIF(UPPER(s) == ".F.", .F., ; + IIF(SUBSTR(s, 1, 1) $ 'Tt', .T., ; + IIF(SUBSTR(s, 1, 1) $ 'Ff', .F., NIL))))) +