Sequence Generator Utility
I sometimes have the need to generate a sequence of values. Whether it be a set of CSS utility classes or a list of fake users, the ability to create a set of incrementing numbers is quite helpful. In SublimeText (my text editor of choice), I can use the Arithmetic features to do this. However, the Arithmetic features use Python, and I'm more of a JavaScript guy. So, I created a sequence generator utility for my site that dynamically evaluates a JavaScript template literal over a custom numeric range and gives me the full power of JavaScript.
Run this sequence utility on BenNadel.com.
View all my utilities on BenNadel.com.
The concept of the utility is simple: I provide a start value, an end value, and a step value in order to configure the range. Then, I define the text of a template literal which will be evaluated in the context of the range iteration. And, since the template literal allows for ${x}
interpolation syntax, I can basically include whatever logic I want.
Within the range iteration, I make several variables available for interpolation:
${ i }
- the sequence value. This is the generated value within the range.${ n }
- the iteration index. This is the 0-based index of the sequence. Basically, it counts from0
toN
.${ nn }
- the inverted index. This is the N-based index of the sequence. Basically, it counts fromN
to0
.
Then, of course, you can use any other JavaScript expression in the interpolation, like:
${ Date.now() }
Once the template literal is evaluated and the interpolation is complete, I provide the option to further evaluate the resultant string as a JavaScript expression. This is helpful when generating JSON and you don't want to have to quote every single key.
Here's the full code for this utility. It's entirely client-side logic and persists the state to the URL. Since this is primarily for my own usage, I won't go into any detail and I present the code as-is.
<cfoutput>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
Sequence Generator - BenNadel.com
</title>
<style type="text/css">
*,
*:before,
*:after {
margin: 0 ;
padding: 0 ;
}
html {
box-sizing: border-box ;
color: ##121212 ;
font-family: monospace ;
font-size: 18px ;
line-height: 1.5 ;
& *,
& *:before,
& *:after {
box-sizing: inherit ;
}
}
body {
margin: 15px 20px 20px 20px ;
}
main {
display: flex ;
flex-direction: column ;
margin: 0 auto ;
max-width: 800px ;
}
fieldset {
border: none ;
}
button,
input:not([type="radio"]):not([type="checkbox"]):not([type="hidden"]),
textarea {
background-color: ##fafafa ;
border: 1px solid ##333333 ;
border-radius: 3px ;
display: block ;
font-family: inherit ;
font-size: inherit ;
line-height: 1.4 ;
padding: 10px 12px ;
}
button {
background-color: ##121212 ;
color: ##f0f0f0 ;
}
textarea {
line-height: 1.7 ;
padding: 11px 15px ;
tab-size: 4 ;
white-space: pre ;
}
h1 {
margin-bottom: 0.7em ;
& a {
color: inherit ;
text-decoration: none ;
&:hover {
text-decoration: underline ;
}
}
}
.description {
display: flex ;
flex-direction: column ;
gap: 1.2em ;
margin-bottom: 2em ;
& code {
background-color: yellow ;
font-weight: bold ;
}
& span {
color: ##999999 ;
}
& hr {}
}
form {
display: flex ;
flex-direction: column ;
gap: 10px ;
}
.range {
display: flex ;
gap: 10px ;
& > div {
display: flex ;
flex-direction: column ;
flex: 1 1 auto ;
gap: 5px ;
}
& label {
font-weight: bold ;
}
& input {
width: 100% ;
}
@media ( width < 800px ) {
flex-direction: column ;
gap: 15px ;
}
}
.modifiers {
display: flex ;
flex-direction: column ;
gap: 5px ;
padding-left: 0.7rem ;
}
.expression {
display: flex ;
flex-direction: column ;
gap: 10px ;
margin-top: 0.5rem ;
& label {
font-weight: bold ;
}
& textarea {
min-height: 200px ;
}
}
.buttons {
align-items: center ;
display: flex ;
gap: 20px ;
margin-top: 0.5em ;
& button {
align-self: flex-start ;
padding: 0.7em 1.5em ;
}
& a {
color: darkred ;
}
}
.result {
display: flex ;
flex-direction: column ;
gap: 10px ;
margin-top: 1rem ;
& label {
font-weight: bold ;
}
& textarea {
min-height: 400px ;
}
}
</style>
</head>
<body>
<main>
<h1>
<a href="/utils">Sequence Generator</a>
</h1>
<section class="description">
<p>
This utility allows a sequence (start...end, inclusive) to be generated using a JavaScript template literal. The input is evaluated in two phases. First, the following variables are made available to the template literal expression:
</p>
<p>
<code>${ i }</code>: sequence value <span>(start, start+step, start+2*step, ...)</span>.<br />
<code>${ n }</code>: iteration index <span>(0, 1, 2, ...)</span>.<br />
<code>${ nn }</code>: inverted iteration index <span>(5, 4, 3, ...)</span>.<br />
</p>
<p>
Then the result of the template literal evaluation can be further evaluated as a JavaScript expression; and, optionally, output as a JSON string.
</p>
<hr />
</section>
<form>
<fieldset class="range">
<div>
<label for="form-start">Start (inclusive):</label>
<input id="form-start" type="number" class="start" />
</div>
<div>
<label for="form-end">End (inclusive):</label>
<input id="form-end" type="number" class="end" />
</div>
<div>
<label for="form-step">Step:</label>
<input id="form-step" type="number" class="step" />
</div>
</fieldset>
<fieldset class="expression">
<label for="form-input">Template Literal:</label>
<textarea id="form-input" class="input"></textarea>
</fieldset>
<fieldset class="modifiers">
<label for="form-evaluateInput">
<input id="form-evaluateInput" type="checkbox" class="evaluateInput" />
Evaluate result as a JavaScript expression.
</label>
<label for="form-renderAsJson">
<input id="form-renderAsJson" type="checkbox" class="renderAsJson" />
Render output using JSON.stringify().
</label>
</fieldset>
<div class="buttons">
<button type="submit" class="generate">
Generate
</button>
<a href="./">
Reset
</a>
</div>
<fieldset class="result">
<label for="form-output">Generated Sequence:</label>
<textarea id="form-output" class="output"></textarea>
</fieldset>
</form>
</main>
<script type="text/javascript">
var DEFAULT_START = "0";
var DEFAULT_END = "100";
var DEFAULT_STEP = "10";
var DEFAULT_INPUT = "Item ${ i }, Iteration ${ n }, Inverted ${ nn }, Now: ${ Date.now() }";
var DEFAULT_EVALUATE_INPUT = false;
var DEFAULT_RENDER_AS_JSON = false;
var formNode = $( "form" );
var startNode = $( ".start" );
var endNode = $( ".end" );
var stepNode = $( ".step" );
var inputNode = $( ".input" );
var evaluateInputNode = $( ".evaluateInput" );
var renderAsJsonNode = $( ".renderAsJson" );
var generateNode = $( ".generate" );
var outputNode = $( ".output" );
$on( formNode, "submit", handleFormSubmit );
$on( inputNode, "keydown", handleKeydown );
$on( window, "popstate", handlePopstate );
loadFromUrl();
// ----------------------------------------------------------------------- //
// ----------------------------------------------------------------------- //
/**
* I generate the sequence from the given inputs.
*/
function handleFormSubmit( event ) {
event.preventDefault();
var start = Number( startNode.value );
var end = Number( endNode.value );
var step = Math.abs( Number( stepNode.value ) );
if (
Number.isNaN( start ) ||
Number.isNaN( end ) ||
Number.isNaN( step ) ||
! step
) {
outputNode.value = "Invalid range inputs.";
return;
}
var size = Math.floor( ( Math.abs( end - start ) / step ) + 1 );
// Stepping down.
if ( start > end ) {
step = -step;
}
var evaluateInput = evaluateInputNode.checked;
var renderAsJson = renderAsJsonNode.checked;
try {
outputNode.value = new Array( size )
.fill( "" )
.map(
( _, n ) => {
// Putting the iteration variables into the global scope
// so that we can use the INDIRECT EVAL, which doesn't
// have access to the local scope. This is a slightly
// better approach for security purposes. Though, none
// really exist in this app - this is mostly for my own
// learning of eval mechanics.
window.n = n;
window.nn = ( size - n - 1 );
window.i = ( start + ( n * step ) );
// NOTE: Indirect eval() used to force global context.
// This way, eval() will only have access to the window
// scope and not any of the lexically-scoped variables.
var element = eval?.( "'use strict';`" + inputNode.value + "`;" );
if ( evaluateInput ) {
element = eval?.( `'use strict';undefined,${ element }` );
}
if ( renderAsJson ) {
element = JSON.stringify( element );
}
return String( element );
}
)
.join( "\n" )
;
persistToUrl();
} catch ( error ) {
outputNode.value = error.message;
}
}
/**
* I handle the keydown event on the input.
*/
function handleKeydown( event ) {
if (
( event.key === "Enter" ) &&
( event.metaKey || event.ctrlKey )
) {
event.preventDefault();
formNode.requestSubmit( generateNode );
}
}
/**
* I handle the history popstate event, reloading the form from the new URL.
*/
function handlePopstate() {
loadFromUrl();
}
/**
* I load the current URL into the form state (and clears the output).
*/
function loadFromUrl() {
var params = new URL( location.href )
.searchParams
;
startNode.value = ( params.get( "start" ) ?? DEFAULT_START );
endNode.value = ( params.get( "end" ) ?? DEFAULT_END );
stepNode.value = ( params.get( "step" ) ?? DEFAULT_STEP );
inputNode.value = ( params.get( "input" ) ?? DEFAULT_INPUT );
evaluateInputNode.checked = params.has( "evaluateInput" )
? ( params.get( "evaluateInput" ) === "true" )
: DEFAULT_EVALUATE_INPUT
;
renderAsJsonNode.checked = params.has( "renderAsJson" )
? ( params.get( "renderAsJson" ) === "true" )
: DEFAULT_RENDER_AS_JSON
;
outputNode.value = "";
}
/**
* I persist the current form state to the URL.
*/
function persistToUrl() {
document.title = `Input: ${ inputNode.value }`;
var params = new URLSearchParams();
params.set( "start", startNode.value );
params.set( "end", endNode.value );
params.set( "step", stepNode.value );
params.set( "input", inputNode.value );
params.set( "evaluateInput", evaluateInputNode.checked );
params.set( "renderAsJson", renderAsJsonNode.checked );
var nextUrl = new URL( location.href );
nextUrl.search = params.toString();
history.pushState( null, null, nextUrl );
}
/**
* I provide a short-hand notation for the query selector.
*/
function $( selector ) {
return document.querySelector( selector );
}
/**
* I provide a short-hand notation for the event binder.
*/
function $on( node, eventType, eventHandler ) {
return node.addEventListener( eventType, eventHandler );
}
</script>
</body>
</html>
</cfoutput>
Some of the mechanics that I'm using here, such as CSS nesting and null coalescing (??
), may not be fully supported in all browsers. But, such is the joy of building things for your own consumption.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →