Skip to content

Commit

Permalink
feature(two-fer): add two-fer practice exercise (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajborla authored May 6, 2024
1 parent 5dfff84 commit 19cbb92
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 0 deletions.
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@
"practices": [],
"prerequisites": [],
"difficulty": 3
},
{
"slug": "two-fer",
"name": "Two-fer",
"uuid": "9e6bfb8a-60e9-4018-9d74-3bdfdb8b5c5a",
"practices": [],
"prerequisites": [],
"difficulty": 2
}
]
},
Expand Down
27 changes: 27 additions & 0 deletions exercises/practice/two-fer/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Instructions

`Two-fer` or `2-fer` is short for two for one.
One for you and one for me.

Given a name, return a string with the message:

```text
One for name, one for me.
```

Where "name" is the given name.

However, if the name is missing, return the string:

```text
One for you, one for me.
```

Here are some examples:

|Name |String to return
|:-------|:------------------
|Alice |One for Alice, one for me.
|Bob |One for Bob, one for me.
| |One for you, one for me.
|Zaphod |One for Zaphod, one for me.
10 changes: 10 additions & 0 deletions exercises/practice/two-fer/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"authors": ["ajborla"],
"files": {
"solution": ["two_fer.prg"],
"test": ["two_fer_test.prg"],
"example": [".meta/example.prg"]
},
"blurb": "Create a sentence of the form \"One for X, one for me.\".",
"source_url": "https://github.com/exercism/problem-specifications/issues/757"
}
12 changes: 12 additions & 0 deletions exercises/practice/two-fer/.meta/example.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
* ----------------------------------------------------------------------------
* exercism.org
* Harbour Track Exercise: two-fer
* Contributed: Anthony J. Borla ([email protected])
* ----------------------------------------------------------------------------

function TwoFer(name)
return ;
IIF(name == NIL .OR. EMPTY(name), ;
"One for you, one for me.", ;
"One for " + ALLTRIM(name) + ", one for me.")

19 changes: 19 additions & 0 deletions exercises/practice/two-fer/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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.

[1cf3e15a-a3d7-4a87-aeb3-ba1b43bc8dce]
description = "no name given"

[b4c6dbb8-b4fb-42c2-bafd-10785abe7709]
description = "a name given"

[3549048d-1a6e-4653-9a79-b0bda163e8d5]
description = "another name given"
153 changes: 153 additions & 0 deletions exercises/practice/two-fer/PRGUNIT.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
* ----------------------------------------------------------------------------
* Harbour Unit Test Worker
* Anthony J. Borla ([email protected])
* ----------------------------------------------------------------------------

#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

8 changes: 8 additions & 0 deletions exercises/practice/two-fer/two_fer.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
* ----------------------------------------------------------------------------
* exercism.org
* Harbour Track Exercise: two-fer
* ----------------------------------------------------------------------------

function TwoFer(name)
return ""

48 changes: 48 additions & 0 deletions exercises/practice/two-fer/two_fer_test.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
* ----------------------------------------------------------------------------
* Harbour Unit Test Runner [two_fer.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, "No name given", "==", "One for you, one for me.", "TwoFer()"
do AddTestDatabase with TESTS, "A name given", "==", "One for Alice, one for me.", "TwoFer('Alice')"
do AddTestDatabase with TESTS, "Another name given", "==", "One for Bob, one for me.", "TwoFer('Bob')"
do AddTestDatabase with TESTS, "Handle arg with spaces", "==", "One for John Smith, one for me.", "TwoFer('John Smith')"
do AddTestDatabase with TESTS, "Name as empty string", "==", "One for you, one for me.", "TwoFer('')"


* 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 "two_fer.prg"

* Unit Test Framework
#include "PRGUNIT.prg"
86 changes: 86 additions & 0 deletions exercises/practice/two-fer/utils.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
* ----------------------------------------------------------------------------
* Harbour Miscellaneous Utilities
* Anthony J. Borla ([email protected])
* ----------------------------------------------------------------------------

#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)))))

0 comments on commit 19cbb92

Please sign in to comment.