Skip to content

Commit

Permalink
feat: new array functions (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
liana-p committed Jul 11, 2023
1 parent 16dbbc0 commit b03afab
Show file tree
Hide file tree
Showing 7 changed files with 481 additions and 7 deletions.
87 changes: 87 additions & 0 deletions docs/commands/all-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,93 @@ Imagine $data.myArray contains an array with [25, 50, 75]
| splice | `splice $data.myArray 1 2 // Returns [50, 75]` | Removes a slice of an array, with the first parameter being the start index and the second being the number of elements to remove. Returns the sliced elements |
| random_from_array | `random_from_array $data.myArray // Returns a random element from the array` | Returns a random element from an array |
| shuffle | `shuffle $data.myArray // Returns a shuffled array` | Shuffles an array |
| entries | `entries $data.myArray // Returns [[0, 25], [1, 50], [2, 75]]` | Returns an array of arrays, each containing the index and value of the original array's elements |

## Array transformation functions

Those functions loop through arrays to perform an operation on each element.
Most of them take a `predicate` parameter, which should be the name of a narrat label that will be called on each element.

The predicate gets given three parameters:

- `element`: The array element for the current iteration
- `index`: The index of the current iteration
- `array`: The array being iterated over

Some of those functions may take different parameters though.

Example:

```narrat
test_arrays:
var simple (new Array "a" "b" "c" "d")
var index (array_find_index $simple test_find)
"Index: %{$index}"
var mapped_array (array_map $simple test_map)
var concatenated (array_join $mapped_array ", ")
"Concatenated: %{$concatenated}"
test_forEach element index array:
"For each: %{$element} %{$index} %{$array}"
test_find element index array:
if (== $element "c"):
return true
else:
return false
test_map element index array:
return (concat $element " mapped")
```

Most of those functions have an API based on similar JavaScript equivalents, which you can find [on the MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/entries).

| Command | Example | Description |
| ---------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| array_find_index | `var index (array_find_index $array test_find)` | Will run the `test_find` label with values passed from the array until it returns true, at which point it will return that index. If nothing is found, it will return -1 |
| array_find | `var element (array_find $array test_find)` | Will run the `test_find` label with values passed from the array until it returns true, at which point it will return that element. If nothing is found, it will return null |
| array_filter | `var filtered (array_filter $array test_filter)` | Will run the `test_filter` label with values passed from the array. Every element for which `test_filter` returns true will be added to the new result array that gets returned at the end |
| array_map | `var mapped (array_map $array test_map)` | Will run the `test_map` label with values passed from the array. A new resulting array is created which gets as values the return values of `test_map` passed for each element |
| array_reduce | `var reduced (array_reduce $array test_reduce 0)` | Will run the `test_reduce` label with values passed from the array. Before the element, index and array parameters the test_reduce function will reduce the current accumulated reduced value, starting with the initial value passed in the initial call (`0` here) |
| array_some | `var some (array_some $array test_some)` | Will run the `test_some` label with values passed from the array. If any of the calls to `test_some` return true, the function will return true. If none of them do, it will return false |
| array_every | `var every (array_every $array test_every)` | Will run the `test_every` label with values passed from the array. If all of the calls to `test_every` return true, the function will return true. If any of them don't, it will return false |

## Object Commands

imagine we have the following object:

```narrat
main:
var test_object (new Object)
set test_object.a "hello"
set test_object.b "world"
```

| Command | Example | Description |
| -------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| object_keys | `var keys (object_keys $array)` | Returns an array with the keys in the object. In this case would return ["a", "b"] |
| object_values | `var values (object_values $array)` | Returns an array with the values in the object. In this case would return ["hello", "world"] |
| object_entries | `var entries (object_entries $array)` | Returns an array with the entries in the object. In this case would return [["a", "hello"], ["b", "world"]] |
| object_has | `var has (object_has $array "a")` | Returns true if the object has the given key, false otherwise |

## For loop commands

Narrat doesn't have proper support for loops yet, but those two commands can help give similar functionality (they work on both arrays and objects).

Imagine we have the following test array:

```narrat
main:
var test_array (new Array "a" "b" "c" "d")
do_things element:
"Element: %{$element}"
```

| Command | Example | Description |
| ------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| for_of | `for_of $test_array do_things` | Will run the `do_things` label once for each element in the array, passing the `element` as the first property. This one iterates over _values_ |
| for_in | `for_in $test_array do_things` | Will run the `do_things` label once for each element in the array, passing the `index` (or `key` if an object) as the first property. This one iterates over _keys_ |

## Time

Expand Down
3 changes: 2 additions & 1 deletion packages/narrat/src/examples/default/scripts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import game from './scripts/default.narrat';
import arrays from './scripts/arrays.narrat';

export default [game];
export default [game, arrays];
16 changes: 16 additions & 0 deletions packages/narrat/src/examples/default/scripts/arrays.narrat
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
test_arrays:
var simple (new Array "a" "b" "c" "d")
var index (array_find_index $simple test_find)
"Index: %{$index}"

test_forEach element index array:
"For each: %{$element} %{$index} %{$array}"

test_find element index array:
if (== $element "c"):
return true
else:
return false

test_map element index array:
return (concat $element " mapped")
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ main:
// jump test_hmr
// jump test_save
// jump test_new_inputs
jump test_arrays
jump quest_demo

test_save:
Expand Down
231 changes: 225 additions & 6 deletions packages/narrat/src/vm/commands/array-commands.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { JUMP_SIGNAL, RETURN_SIGNAL, STOP_SIGNAL } from '@/constants';
import { useMain } from '@/stores/main-store';
import { SetFrameOptions, useVM } from '@/stores/vm-store';
import { error } from '@/utils/error-handling';
import { commandLog, commandRuntimeError } from './command-helpers';
import { useVM } from '@/stores/vm-store';
import { commandRuntimeError } from './command-helpers';
import { CommandPlugin } from './command-plugin';

export const shuffleCommand = CommandPlugin.FromOptions<{ array: any[] }>({
Expand Down Expand Up @@ -39,7 +36,6 @@ export const pushCommand = CommandPlugin.FromOptions<{
},
});

// Create array popCommand, joinCommand, concatCommand, includesCommand, reverseCommand, shiftCommand, sliceCommand
export const popCommand = CommandPlugin.FromOptions<{ array: any[] }>({
keyword: 'pop',
argTypes: [{ name: 'array', type: 'any' }],
Expand All @@ -66,6 +62,26 @@ export const shiftCommand = CommandPlugin.FromOptions<{ array: any[] }>({
},
});

export const unshiftCommand = CommandPlugin.FromOptions<{
array: any[];
value: any;
}>({
keyword: 'unshift',
argTypes: [
{ name: 'array', type: 'any' },
{ name: 'value', type: 'any' },
],
runner: async (cmd) => {
const { array, value } = cmd.options;
if (!Array.isArray(array)) {
commandRuntimeError(cmd, `requires an array argument`);
return;
}
array.unshift(value);
return array;
},
});

export const joinCommand = CommandPlugin.FromOptions<{
array: any[];
separator?: string;
Expand Down Expand Up @@ -225,6 +241,8 @@ export const arrayFindIndexCommand = CommandPlugin.FromOptions<{
const predicateResult = await useVM().runLabelFunction(
predicateLabel,
element,
index,
array,
...cmd.args.slice(2),
);
if (predicateResult === true) {
Expand All @@ -235,6 +253,207 @@ export const arrayFindIndexCommand = CommandPlugin.FromOptions<{
},
});

export const arrayFindCommand = CommandPlugin.FromOptions<{
array: any[];
predicateLabel: string;
}>({
keyword: 'array_find',
argTypes: [
{ name: 'array', type: 'any' },
{ name: 'predicateLabel', type: 'string' },
{ name: 'rest', type: 'rest', optional: true },
],
runner: async (cmd) => {
const { array, predicateLabel } = cmd.options;
if (!Array.isArray(array)) {
commandRuntimeError(cmd, `requires an array argument`);
}
for (const [index, element] of array.entries()) {
const predicateResult = await useVM().runLabelFunction(
predicateLabel,
element,
index,
array,
...cmd.args.slice(2),
);
if (predicateResult === true) {
return element;
}
}
return null;
},
});

export const arrayFilterCommand = CommandPlugin.FromOptions<{
array: any[];
predicateLabel: string;
}>({
keyword: 'array_filter',
argTypes: [
{ name: 'array', type: 'any' },
{ name: 'predicateLabel', type: 'string' },
{ name: 'rest', type: 'rest', optional: true },
],
runner: async (cmd) => {
const { array, predicateLabel } = cmd.options;
if (!Array.isArray(array)) {
commandRuntimeError(cmd, `requires an array argument`);
}
const result = [];
for (const [index, element] of array.entries()) {
const predicateResult = await useVM().runLabelFunction(
predicateLabel,
element,
index,
array,
...cmd.args.slice(2),
);
if (predicateResult === true) {
result.push(element);
}
}
return result;
},
});

export const arrayMapCommand = CommandPlugin.FromOptions<{
array: any[];
mapperLabel: string;
}>({
keyword: 'array_map',
argTypes: [
{ name: 'array', type: 'any' },
{ name: 'mapperLabel', type: 'string' },
{ name: 'rest', type: 'rest', optional: true },
],
runner: async (cmd) => {
const { array, mapperLabel } = cmd.options;
if (!Array.isArray(array)) {
commandRuntimeError(cmd, `requires an array argument`);
}
const result = [];
for (const [index, element] of array.entries()) {
const predicateResult = await useVM().runLabelFunction(
mapperLabel,
element,
index,
array,
...cmd.args.slice(2),
);
result.push(predicateResult);
}
return result;
},
});

export const arrayReduceCommand = CommandPlugin.FromOptions<{
array: any[];
reducerLabel: string;
initValue: any;
}>({
keyword: 'array_reduce',
argTypes: [
{ name: 'array', type: 'any' },
{ name: 'reducerLabel', type: 'string' },
{ name: 'initValue', type: 'any' },
{ name: 'rest', type: 'rest', optional: true },
],
runner: async (cmd) => {
const { array, reducerLabel, initValue } = cmd.options;
if (!Array.isArray(array)) {
commandRuntimeError(cmd, `requires an array argument`);
}
let result = initValue;
for (const [index, element] of array.entries()) {
result = await useVM().runLabelFunction(
reducerLabel,
result,
element,
index,
array,
...cmd.args.slice(3),
);
}
return result;
},
});

export const arraySomeCommand = CommandPlugin.FromOptions<{
array: any[];
predicateLabel: string;
}>({
keyword: 'array_some',
argTypes: [
{ name: 'array', type: 'any' },
{ name: 'predicateLabel', type: 'string' },
{ name: 'rest', type: 'rest', optional: true },
],
runner: async (cmd) => {
const { array, predicateLabel } = cmd.options;
if (!Array.isArray(array)) {
commandRuntimeError(cmd, `requires an array argument`);
}
for (const [index, element] of array.entries()) {
const predicateResult = await useVM().runLabelFunction(
predicateLabel,
element,
index,
array,
...cmd.args.slice(2),
);
if (predicateResult === true) {
return true;
}
}
return false;
},
});

export const arrayEveryCommand = CommandPlugin.FromOptions<{
array: any[];
predicateLabel: string;
}>({
keyword: 'array_every',
argTypes: [
{ name: 'array', type: 'any' },
{ name: 'predicateLabel', type: 'string' },
{ name: 'rest', type: 'rest', optional: true },
],
runner: async (cmd) => {
const { array, predicateLabel } = cmd.options;
if (!Array.isArray(array)) {
commandRuntimeError(cmd, `requires an array argument`);
}
for (const [index, element] of array.entries()) {
const predicateResult = await useVM().runLabelFunction(
predicateLabel,
element,
index,
array,
...cmd.args.slice(2),
);
if (predicateResult !== true) {
return false;
}
}
return true;
},
});

export const arrayEntriesCommand = CommandPlugin.FromOptions<{
array: any[];
}>({
keyword: 'array_entries',
argTypes: [{ name: 'array', type: 'any' }],
runner: async (cmd) => {
const { array } = cmd.options;
if (!Array.isArray(array)) {
commandRuntimeError(cmd, `requires an array argument`);
}
return array.entries();
},
});

function shuffleArray(array: any[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
Expand Down
Loading

0 comments on commit b03afab

Please sign in to comment.