const seeded = Fiona(1322672)

Overview

Fiona is a helper for generating large chunks of seeded random data. It is based around a PRNG that makes a mockery of creating predictable authentic looking random data.

Fiona can be used to simulate random data conforming to defined structures, making testing large datasets predictable and developing against future changes a little easier.

// input

const Fiona = require('fiona')

const seeded = Fiona(1322672)

const data = seeded.object({
  name: Fiona.Fullname,
  age: Fiona.Number({ max: 100 })
})

console.log(data)
// output

{"name":"Miss Fiona Moon","age":5}

n.b. you can change the seed by choosing the dots in the page banner, choosing the same seed will result in the same data

How Fiona works

The XOR shift PRNG at Fiona's core generates on demand a set of pseudo random data.

For a given seed, the generated data is identical, changing the seed changes the data.

The data appears to be random, but is deterministic.

When a seed is passed to Fiona, it returns a seeded instance with data generation methods.

Each time a method needs random data, the seeded instance carves off a chunk from the PRNG.

Complex data structures can be built up in a repeatable way.

Fiona clones the seeded instance for each piece of data, seeding the clone with a combination of the original seed, and the data path. This means that adding to the data structure does not change existing values.

The data generated remains consistent even with updates to the structure.

Getting started

Install via npm

npm install fiona

import into your app

import Fiona from 'fiona'

or include in webpage from cdn

<script src='https://cdn.jsdelivr.net/npm/fiona'></script>

n.b. you can open the console and edit/run code samples from this page

The most basic use case is to generate a random number

Fiona().number()
// input

const milesFromHome = Fiona(1322672).number()
const age = Fiona(1322672).number({ max: 100 })
// output

267715
27

It becomes more useful when creating data structures. With registered functions, you can use the shorthand capitalized Constructors within data structures for a very terse syntax.

// input

Fiona(1322672).object({
  milesFromHome: Fiona.Number,
  age: Fiona.Number({ max: 100 })
})
// output

{
  "milesFromHome": 381915,
  "age": 5
}

As requirements develop, you can add values to the structure without the original values changing, this is because the seeded random number generator uses a combination of the seed Fiona is initialised with, and the pathname of the property being resolved.

// input

Fiona(1322672).object({
  milesFromHome: Fiona.Number,
  name: Fiona.Fullname,
  age: Fiona.Number({ max: 100 })
})
// output

{
  "milesFromHome": 381915,
  "name": "Miss Fiona Moon",
  "age": 5
}

There are lots of methods to help generate different types of data, you can read more about them in the api section. These methods can be called on an instance to return a value, or on `Fiona` itself to return a higher order function that when called with an instance, returns a value. Also, during recursion, any found functions are executed to resolve their value. Combining these things allows a very terse and powerful syntax to describe any data structure.

// use a regex pattern to generate strings that match
const ibanPattern = /[A-Z]{2}\d{2}( \d{4}){4,5} \d{1,3}/
// input

Fiona(1322672).object({
  milesFromHome: Fiona.Number,
  age: Fiona.Number({ max: 100 }),
  name: Fiona.Fullname,
  iban: ibanPattern,
  colour: Fiona.OneOf([
    'red',
    'yellow',
    'blue'
  ])
})
// output

{
  "milesFromHome": 381915,
  "age": 5,
  "name": "Miss Fiona Moon",
  "iban": "SL24 7149 0901 0330 4218 9",
  "colour": "blue"
}

Seeded pseudo random number generator

At it's core, Fiona has a seeded prng that will give back approximately evenly distributed floating point numbers between 0 and 1 just like Math.random, but in a pre-determined order based on the seed. The seed defaults to Math.random() but can be passed in during initialisation when you want consistent output.

// input

const seeded = Fiona(1322672)
seeded.number()
seeded.number()
// output

// same seed produces same results each time
821136
284283

n.b. you can change the seed by choosing the dots in the page banner

The prng sets the initial seed when Fiona is initialised, then tracks new seeds generated in consistent sequence internally. The seed can be reset to the initial value, or any arbitrary value at any time. This makes it easy to ensure data is repeatable.

// input

const seeded = Fiona(1322672)

seeded.number()
seeded.reset(1322673)
seeded.number()
seeded.reset()
seeded.number()
// output

15

46

15

Distribution

Adding distribution to the output of random allows for powerful manipulation of the results. For example, the distribution of income is not even, where many more people are on low income than high. If we simply choose from 10,000 to 1,000,000 then the average income would be around 505,000 which is unrealistically high. If we add distribution, we can make this represent our target distribution much more accurately so the top and bottom income remain the same, but the values tend to be much closer to the bottom end of the scale.

// input

const tendToLow = i => i * i * i
const income = seeded => seeded.distribution(tendToLow).number({ max: 100000 })

Fiona(1322672).array(5, income)
// output

[55366,2297,17072,42742,20508]

The distribution function takes a floating point number from 0-1 and returns a number from 0-1. Bezier easing functions are a nice way to shape your data.

// input

import bezierEasing from 'bezier-easing'

const salaryDistribution = bezierEasing(0.5, 0.5, 1, 0)
const income = seeded => seeded.distribution(salaryDistribution).number({ max: 100000 })

Fiona(1322672).array(5, income)
// output

[44228,27552,34917,40488,35670]

You can also clamp or otherwise manipulate the output with distribution functions. In this example, any salary that would have been less than 40k will be 40k because the number method is based on a call to random which has been passed through the distribution function

// input

Fiona(1322672).array(5, seeded => seeded.distribution(i => i < 0.4 ? 0.4 : i).number({ max: 100000 })
// output

[82114,40000,55475,75328,58971]

Extending

It is easy to extend `Fiona` for both one off operations and by registering custom methods that will integrate witht the core library. Pull requests adding more general use functionality very welcome.

During recursion of data structures, any functions encountered are called with the current instance, and resolved to their value, recursively. This means you can easily define a pure function with custom logic, and pass it into a data structure to be evaluated inline.

// input

const income = seeded => seeded.distribution(i => i * i * i).number({ max: 1000000 })

Fiona(1322672).object({ name: Fiona.Fullname, income })
// output

{
"name": "Miss Fiona Moon",
  "income": 57773
}

Use the `register` method to add your function as a method in Fiona.

// input

Fiona.register(['income', income])

Fiona(1322672).income()
// output

19187

Arguments are passed through to your registered function too.

// input

Fiona.register(['chooseThree', (seeded, arr) => seeded.choose(3, arr)])

Fiona(1322672).chooseThree([1, 2, 3, 4, 5, 6, 7, 8, 9])
// output

[3,7,8]

Registered functions also have a factory function added to the root `Fiona` function which uses a capitalized form of the method. This is a convenient short form that can be used in recursion.

// input

Fiona.register(['chooseThree', (seeded, arr) => seeded.choose(3, arr)])

Fiona(1322672).object({
    threeNumbers: Fiona.ChooseThree([1, 2, 3, 4, 5, 6, 7, 8, 9])
})

Fiona(1322672).object({
    threeNumbers: seeded => seeded.chooseThree([1, 2, 3, 4, 5, 6, 7, 8, 9])
})
// output

{"threeNumbers":[2,5,8]}
{"threeNumbers":[2,5,8]}

Contributing

Fiona aims to be performant, robust, well documented, well organised, well tested and small (~10KB gzipped).

Fiona is open source, fork me on github. Pull requests and issues welcome. The most useful thing the community could contribute to this project at this time is to help build up the methods to become a rich tapestry of data generating utilities.

The code style is terse and succinct, and hopefully easy to understand and work with. The file structure is based around colocation, so the src folder includes the main library code, it's tests, and the documentation.

Whilst there are very few dependencies (only randexp) in the Fiona library, there are several used to aid development:

  • deno for developing, linting, testing and bundling
  • react and next for documentation website

Updating or creating a pull request will trigger a deployment using vercel's now service, so you can preview the suggested changes in a production like environment.