For loop overwriting object values within links

Please specify version and format if asking for help, or apply optional tags above:
Twine Version: 2.3.5
Story Format: Sugarcube

First question is whether <<set $products to $products.sort()>> can sort an object alphanumerically by their name: value.

The next part of the code works, it just lists out each item and its cost.

The problem is when I try to include a “Buy” link after each product. The for loop sets the item’s attributes to the last item on the list – meaning when Player clicks “Buy” for a $2.00 item, they get charged for a $.50 item because it was the last in the for loop.

This story was converted from Harlowe, and I think I solved this issue with link-repeat, but Sugarcube doesn’t have any link-repeat as far as I can tell.

<<set $products to $products.sort()>>
	<<for _item range $products>>
		<<if _item.name != "food" && _item.name != "wood">>
			<<-ucfirst( _item.name)>>s: $<<-_item.cost>>
		<<else>>
			<<-ucfirst( _item.name)>>: $<<-_item.cost>>
		<</if>>
	<<link "Buy">>
		<<if $dollars >= _item.cost>>
			<<set _item.number += 1>>
			<<set $dollars -= _item.cost>>
			<<replace "#buy">><<include "Buy List">><</replace>>
		<</if>>
	<</link>>
1 Like

I will assume that the Array value within $products looks something like…

<<set $products to [
	{name: "food", cost: 5, number: 0},
	{name: "wood", cost: 10, number: 0},
	{name: "thing", cost: 15, number: 0}
]>>

As explained within the JavaScript <Array>.sort() function’s documentation you can pass a ‘Compare Function’ as the first argument, and the code examples within the function’s Description section includes one which demonstrates how to sort an Array of Generic Objects that each have a name property.

Implementing that example within a Passage using your $products Array would look something like…

<<set $products to $products.sort(function (a, b) {
	var nameA = a.name.toUpperCase();
	var nameB = b.name.toUpperCase();

	if (nameA < nameB) {
		return -1;
	}
	if (nameA > nameB) {
		return 1;
	}

	/* names must be equal */
	return 0;
})>>

As you have discovered the variables referenced within the body of a <<link>> macro are not resolved/evaluated until after that link is selected, and at that time the your _item (temporary) variable will either be equal to last value assigned to that variable or won’t exist if the temporary variable has gone ‘out of scope’. You can use the <<capture>> macro to overcome this behaviour.

<<for _item range $products>>
	<<if _item.name != "food" && _item.name != "wood">>
		<<- ucfirst( _item.name) + "s: $" + _item.cost>>
	<<else>>
		<<- ucfirst( _item.name) + ": $" + _item.cost>>
	<</if>>
	<<capture _item>>
		<<link "Buy">>
			<<if $dollars >= _item.cost>>
				<<set _item.number += 1>>
				<<set $dollars -= _item.cost>>
				<<replace "#buy">><<include "Buy List">><</replace>>
			<</if>>
		<</link>>
	<</capture>>
<</for>>

You may of notice that I replace the double calling of the <<->> macro for each line that displays the product name & cost with a single call of that macro that was passed a composite String value.

eg. I replaced <<- ucfirst( _item.name)>>s: $<<-_item.cost>>
with <<- ucfirst( _item.name) + "s: $" + _item.cost>>

I did this because there is a cost involved in calling a macro, so making that change I was able to reduce the number of times the <<->> macro was called by 1 per loop iteration.

2 Likes

Thanks! I would never have guessed there would be a capture macro if you hadn’t told me.

That depends on what $products is. If it’s an array, then yes, as Greyelf showed. If it’s a generic object, then no.

Generic objects, by their very nature, are unordered. You can’t sort them because of that. Only ordered lists, like arrays (which are a specific kind of object), can be sorted.

If you need the items in a generic object sorted by their key (what you called the “name” above), then you could make a temporary array of the keys sorted as strings like this:

<<set _productList to Object.keys($products).sort()>>

You could then use that ordered array to access items on the $products object sorted by key.

Greyelf answered your other question.

Also, I note that you’re using <<->> as the shortcut for <<print>>, when you probably should use the <<=>> shortcut instead. The former will simply display any HTML code or SugarCube macros within it as text, while the latter will add them as functioning code. For a simple example of that, this:

<<set $test = "back">>
<<- '<<' + $test + '>>'>>

would simply display the text “<<back>>”, while this:

<<set $test = "back">>
<<= '<<' + $test + '>>'>>

would display a functioning “Back” link instead.

Hope that helps! :slight_smile:

1 Like