Questions about working with arrays in Harlowe (3.3.5) - string matching

Twine Version: 2.6.2
Harlowe 3.3.5

Hi all,

I am having some problems with understanding how arrays work and implementing them in my story, I am hoping that you can help?

I have set up an array to track the information that a player learns in my story, and I would like to be able to check whether a particular piece of information is present in that array. (& then to modify the next steps accordingly - i.e., if they haven’t learned X, the story will prompt them to go to the correct passage to learn X)

Somehow the structure and commands for working with arrays just don’t make sense in my brain no matter how many times I read the documentation, so apologies if this is a really stupid question.

This is basically the sort of thing I am trying to set up, copied below (though the strings in the array in practice would be picked up in different passages - but I’ve put it all in one to test it).

(set: $test to (a: "Gram stain is positive", "Malachite green is negative", "catalase is positive"))

(set: $test to it + (a: "lactose fermentation is positive"))

(if: $test contains "lactose")[Yay!]
(else:)[Nope] 

(if: $test contains "lactose fermentation is positive")[Hello!]
(else:)[Woe!]

When I try running this code, it prints “Nope” and “Hello!” - it seems to only work if I am matching an entire string in an array.

Is this correct? Is there a way (that isn’t too complicated) to look for a partial string in an array? (I would like to be able to search for whether they’ve done the test, regardless of the result - which is why I don’t want to just type out the entire string… I was hoping that the first if statement above would have printed “Yay!”)

[I can’t do this by checking whether the passage with the test is in the player’s history, because of the way that I’ve set the story up. Each test page has different options that are conditionally revealed depending on the player’s choices + some amount of randomisation, it wouldn’t be feasible to set up a passage for each of the different test results.]

Thanks very much for any help that you can give!!

This is a tricky one. It looks like the problem is that when you search for “lactose” in an array, you’re searching for matching items in the array, so it won’t find matches within those items (even if they’re there). Instead, you need to run a check for each item (to see if there’s a match within that string). Try this example instead: my Woohoo!/Boo! addition should hopefully work the way you want:

(set: $test to (a: "Gram stain is positive", "Malachite green is negative", "catalase is positive"))

(set: $test to it + (a: "lactose fermentation is positive"))

(if: $test contains "lactose")[Yay!](else:)[Nope]

(for: each _item where it contains "lactose", ...$test)[Woohoo!](else:)[Boo!]

(if: $test contains "lactose fermentation is positive")[Hello!](else:)[Woe!]

It may be worth noting that I believe this would print “Woohoo!” multiple times in a row if “lactose” appeared in more than one item. If that’s something you want to avoid, you may have to either phrase things carefully to avoid double-matches, or have those matches set a $testItemFound variable to true (and then display the text if that variable is true - since it won’t matter if it’s set to true more than once in a row).

2 Likes

That seems to work perfectly!! Thank you so much! (I guess the … spools out all of the items in the array and the for loop tells it to check everything in the unspooled items? I don’t know why I have such a block when it comes to understanding arrays!)

Thanks for pointing out the problem with the code potentially acting multiple times, that could totally happen with the way I’ve got things set up in my story. Posting my solution below, just in case someone comes along in a few months with a similar problem and also finds it helpful: this prints the desired text only once (even though “lactose” is in the array twice).

(set: $test to (a: "Gram stain is positive", "Malachite green is negative", "catalase is positive"))
(set: _lactose to 0)

(set: $test to it + (a: "lactose fermentation is positive"))
(set: $test to it + (a: "lactose fermentation is positive"))

(for: each _item where it contains "lactose", ...$test)[(set: _lactose to it + 1)]

(if: _lactose > 0)[You should test for aerobic growth]
(else:)[You should test for lactose fermentation!] 

Thanks ever so much for your help, I really really appreciate it!!

2 Likes

The three consecutive full-stops ... is known as a spread operator, and it is mentioned in the Array data documentation.

The operator basically converts the Array of items into a sequence of arguments to be passed to the macro.
eg. if you had an Array like the following…

(set: $fruits to (a: "Apple", "Banana", "Cherry", "Date"))

…then using the spread operator like so…

(for: each _item, ...$fruits)[ _item ]

…is the functional equivalent of…

(for: each _item, "Apple", "Banana", "Cherry", "Date")[ _item ]

re: Why (if: $test contains "lactose") doesn’t work as you expected.

The Array variation of the contains comparison operator compares the entire contents of each Array element against the right side value using an exactly equals to equality test.

eg. that (if:) macro call is functionally the equivalent of…

(set: $found to false)
(for: each _item, ...$test)[
	(if: _item is "lactose")[
		(set: $found to true)
	]
]

…and none of the String elements in your array are exactly equal to “lactose”.

What you functional want would be the equivalent of…

(set: $found to false)
(for: each _item, ...$test)[
	(if: _item contains "lactose")[
		(set: $found to true)
	]
]

…which is using the String variation of the contains operator to determine if any of the String elements in array contains the word “lactose”.

note: The slightly more advanced (some-pass: ) macro could be use to achieve the outcome you were trying to achieve…

(if: (some-pass: _item where _item contains "lactose", ...$test))[Yay!]
(else:)[Nope] 

…but that would required learning about Harlowe’s Lambda data type. The above is basically doing the same looping through the Array’s elements as the previous (for:) macro example, and applying a similar String related contains comparison against each String element.

2 Likes

As an aside, you might be better using a data map (dm:) for this sort of information than an array, because that would allow you to associate the result of each test with the test, and remove the need to match text within the result.

I’m thinking something like this (example in Twee notation, where :: indicates a passage, and [] a list of tags)

:: startup tagged passage [startup]
(set: $testList to (dm:
   "gram stain", false,
   "malachite green", false,
   "catalase", false,
   "lactose fermentation", false
))

:: gram stain test passage
(set: $testList's "gram stain" to true)

:: passage where you test gram stain
(if: $testList's "gram stain")[
    Gram Stain test was positive
]

List test results in a nice bullet-point list
{<ul>(for: each _test, ...(dm-names: $test))[
	<li>_test: (print: $test's _test)</li>
]</ul>}

If you want to also track if the test was done at all, then instead of using true and false you could set the tests all to “not tested” in the startup passage, and then to “postive” and “negative” later

:: startup tagged passage [startup]
(set: $testList to (dm:
   "gram stain", "not tested",
   "malachite green", "not tested",
   "catalase", "not tested",
   "lactose fermentation", "not tested"
))

:: gram stain test passage
(set: $testList's "gram stain" to "positive")

:: passage where you test gram stain
(if: $testList's "gram stain" is "positive")[
    Gram Stain test was positive
]

List test results in a nice bullet-point list
{<ul>(for: each _test, ...(dm-names: $test))[
	<li>_test: (print: $test's _test)</li>
]</ul>}

2 Likes

Thanks for explaining so patiently Greyelf! I am sure arrays with stick in my brain one of these days, though the number of times I’ve read through the documentation and walked away still not understanding … suggests that it is going to be a slow process, at best.

One thing I am really struggling to grasp with these codes is how, when you use the spread operator on the array (for example:

(set: $fruits to (a: "Apple", "Banana", "Cherry", "Date"))
(for: each _item, ...$fruits)[ _item ]

how does the computer “know” that _item refers to the items in the spread array? Afaik, _item is just a temporary variable named item that can be used in that passage.

[I tried running the same code but with both instances of “_item” changed to “_fruit” and it still worked. So there is nothing special about _item? Computers are so weird…]

That’s just the syntax of Harlowe’s (for:) whatever comes after the each is a temporary variable that holds the value each time you go around the loop. You could call it anything.

1 Like

Ah, I see … yes, I think that would probably have been the most sensible way to go about doing it! Having already set up the tests (43 of them at current count) I … might not go back and rewrite them all in a datamap. Unless there are massive negative consequences from using the array instead of the datamap?

I can see how it would have been easier, if I had started out with a datamap and entered all the values from each of the tests, I could have kept them all neatly together in one passage instead of strewing them across 43 separate ones. This project has sort of grown organically far beyond what I imagined when I started it!

Thank you, though, for the aside, and for explaining - I think I have a clearer idea of how to work with datamaps because of it! and a better idea of how to do things for my next project!

1 Like

I see! Thanks so much for explaining!