<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title></title>
    <link rel="self" type="application/atom+xml" href="/atom.xml"/>
    <link rel="alternate" type="text/html" href="/"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-08-23T00:00:00+00:00</updated>
    <id>/atom.xml</id>
    <entry xml:lang="en">
        <title>Terms</title>
        <published>2026-08-23T00:00:00+00:00</published>
        <updated>2026-08-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            Paul Richards
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/terms/"/>
        <id>/pages/terms/</id>
        
        <content type="html" xml:base="/pages/terms/">&lt;p&gt;&lt;em&gt;Last updated: August 23, 2026&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;general-character-of-the-website&quot;&gt;General character of the website&lt;/h2&gt;
&lt;p&gt;This website is a &lt;strong&gt;private, non-commercial blog&lt;/strong&gt; with mostly informational content. It can be used &lt;strong&gt;without registration&lt;/strong&gt;
and &lt;strong&gt;without providing personal data&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The following notes are for transparency and clarity. They are &lt;strong&gt;not general terms and conditions&lt;/strong&gt; and &lt;strong&gt;not a contract&lt;/strong&gt;
in a legal sense.&lt;/p&gt;
&lt;h2 id=&quot;content-and-opinions&quot;&gt;Content and opinions&lt;/h2&gt;
&lt;p&gt;The content published on this website reflects &lt;strong&gt;my personal views and experiences&lt;/strong&gt;. It is &lt;strong&gt;not professional advice&lt;/strong&gt;,
in particular not legal, tax, medical, or financial advice.&lt;/p&gt;
&lt;p&gt;I aim to provide correct and current information but make &lt;strong&gt;no warranties&lt;/strong&gt; regarding accuracy, completeness, or suitability
for a specific purpose.&lt;/p&gt;
&lt;h2 id=&quot;liability&quot;&gt;Liability&lt;/h2&gt;
&lt;p&gt;Liability for damages arising from use of, or reliance on, the information on this website is excluded to the extent
permitted by law.&lt;/p&gt;
&lt;p&gt;Nothing in these notes excludes or limits liability for &lt;strong&gt;death or personal injury caused by negligence&lt;/strong&gt;, or for
&lt;strong&gt;fraud&lt;/strong&gt;, or any other liability that cannot be excluded under English law.&lt;/p&gt;
&lt;h2 id=&quot;external-links&quot;&gt;External links&lt;/h2&gt;
&lt;p&gt;This website may contain links to external websites of third parties. I have no influence on their content.&lt;/p&gt;
&lt;p&gt;The provider or operator of the linked pages is always responsible. At the time of linking, no illegal content was apparent.
If legal violations become known, the links will be removed promptly.&lt;/p&gt;
&lt;h2 id=&quot;copyright-and-use-of-content&quot;&gt;Copyright and use of content&lt;/h2&gt;
&lt;p&gt;The content and works published on this website are subject to UK copyright law (Copyright, Designs and Patents Act 1988).&lt;/p&gt;
&lt;p&gt;Unless stated otherwise, &lt;strong&gt;copying and redistribution&lt;/strong&gt; are permitted &lt;strong&gt;as long as the author&#39;s attribution notice remains&lt;/strong&gt; and the use is not commercial.&lt;/p&gt;
&lt;p&gt;Any further use requires my prior consent.&lt;/p&gt;
&lt;h2 id=&quot;privacy&quot;&gt;Privacy&lt;/h2&gt;
&lt;p&gt;Information about the processing of personal data and browser storage can be found in the
&lt;a href=&quot;/pages/privacy/&quot;&gt;privacy policy&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;applicable-law&quot;&gt;Applicable law&lt;/h2&gt;
&lt;p&gt;These notes and the use of this website are governed by the laws of England and Wales.&lt;/p&gt;
&lt;h2 id=&quot;closing-note&quot;&gt;Closing note&lt;/h2&gt;
&lt;p&gt;If any part of these notes is or becomes invalid, the remaining content remains effective.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Chasing a five-year-old Windows-only race condition in Zola</title>
        <published>2026-08-18T00:00:00+00:00</published>
        <updated>2026-08-18T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/blog/zola-windows-bug/"/>
        <id>/blog/zola-windows-bug/</id>
        
        <content type="html" xml:base="/blog/zola-windows-bug/">&lt;p&gt;A few weeks ago I started to run into constant build failures in &lt;a rel=&quot;external&quot; href=&quot;https://www.getzola.org&quot;&gt;Zola&lt;/a&gt;, the Rust static site generator that I planned to use for this blog:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Error: Was not able to copy file ...\content\entertainment\filename\2.png to ...\public\entertainment\filename\2.png&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Reason: Another program is using this file and the process cannot access it (os error 32)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows error 32 is &lt;code&gt;ERROR_SHARING_VIOLATION&lt;/code&gt; — it means another process (or another thread in the &lt;em&gt;same&lt;/em&gt; process) had the file open when something tried to overwrite it. The build didn&#39;t fail every time, which is the classic signature of a race condition: sometimes the timing lines up badly, sometimes it doesn&#39;t.&lt;/p&gt;
&lt;p&gt;A quick search turned up &lt;a rel=&quot;external&quot; href=&quot;https://github.com/getzola/zola/issues/1599&quot;&gt;issue #1599&lt;/a&gt;, open since 2021, reported by another Windows user hitting the exact same error on a page with a dozen &lt;code&gt;resize_image()&lt;/code&gt; calls. It had sat there for years labeled &quot;Need Windows help&quot; — understandable, since intermittent, platform-specific races are miserable to debug from a distance, and it seems that Zola&#39;s main maintainer does not run Windows day to day.&lt;/p&gt;
&lt;h2 id=&quot;reproducing-it&quot;&gt;Reproducing it&lt;/h2&gt;
&lt;p&gt;I grabbed a copy of zola, set up a rust deelopment environment, and with a bit of help from our AI friends - set up a repository to troubleshoot the issue - &lt;a rel=&quot;external&quot; href=&quot;https://github.com/mantis/zola/commit/5c278ee5d6067f479ee8f4d825b45a21c138b436&quot;&gt;my repository&lt;/a&gt;. I started out instrumenting Zola&#39;s file-copy code directly, since the error message alone didn&#39;t say much beyond &quot;something else has this file open.&quot; I had my AI assistant add:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a small &lt;code&gt;dbgfs!&lt;/code&gt; macro that logs a timestamp and thread ID alongside each filesystem operation, so I could see which threads were touching which paths and in what order&lt;/li&gt;
&lt;li&gt;a retry-with-backoff wrapper around the copy/remove/hard-link calls, specifically catching raw OS error 32 and giving Windows a few tens of milliseconds to release the lock before giving up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The retry wrapper idea came from reviewing github and spotting that Tauri does something similar - they found that antivirus services were keeping the file locked. This was a temporary issue to try and seew whether we could get past the lock — I flagged it in the commit as temporary scaffolding to confirm the theory, not something to actually ship. But the debug logging was the useful part: it showed two threads copying the &lt;em&gt;same&lt;/em&gt; asset to the &lt;em&gt;same&lt;/em&gt; destination path at effectively the same moment.&lt;/p&gt;
&lt;p&gt;At this point, it pointed at Zola&#39;s and it&#39;s build system itself being the issue - rather then a 3rd party (antivirus / indexing etc). Great news!. The AI assistant did a good job at this point of working out what was going on - Pages are rendered and their assets copied in parallel via &lt;a rel=&quot;external&quot; href=&quot;https://github.com/rayon-rs/rayon&quot;&gt;rayon&lt;/a&gt;, and each page is normally queued exactly once. But Zola has a &lt;code&gt;transparent&lt;/code&gt; section feature, where a page can effectively belong to more than one section in the content tree. So, between myself, the AI, the other users on the bug and the zola developer - they had a working theory that a page under a transparent section was showing up in the page list of &lt;em&gt;every&lt;/em&gt; transparent ancestor section, not just its immediate parent — so it was getting pushed onto the job queue once per ancestor. On Linux and macOS, two threads writing the same bytes to the same path at nearly the same time is harmless. On Windows, it&#39;s a sharing violation.&lt;/p&gt;
&lt;p&gt;I left the diagnosis and the repro for the maintainers, since confirming and fixing it properly needed someone who understand the project and the RUST programming language. My work was done - I could build my set, and the developers hopefully had everything they needed to implement the proper fix.&lt;/p&gt;
&lt;h2 id=&quot;the-real-fix&quot;&gt;The real fix&lt;/h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/Keats&quot;&gt;Keats&lt;/a&gt; picked it up and landed the fix in &lt;code&gt;next&lt;/code&gt;: &lt;a rel=&quot;external&quot; href=&quot;https://github.com/getzola/zola/commit/c1d0438d06442e785be60e5fe9c5e4f8fce2433a&quot;&gt;&lt;code&gt;c1d0438&lt;/code&gt;&lt;/a&gt;, &quot;Render pages only once when transparent=true.&quot; It confirmed the theory — his commit message points straight at #1599.&lt;/p&gt;
&lt;p&gt;The fix itself is small. &lt;code&gt;add_section_jobs()&lt;/code&gt; now takes a &lt;code&gt;seen_pages: &amp;amp;mut HashSet&amp;lt;&amp;amp;&#39;a Path&amp;gt;&lt;/code&gt; parameter, and every page gets checked against it before being pushed as a job:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;if&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;render &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; seen_pages&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;insert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1 z-d-1 z-d-i&quot;&gt;    self&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;jobs&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;Page&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;HashSet::insert&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; if the value was already present, so this one line does double duty: it&#39;s both the &quot;have we seen this page already&quot; check and the &quot;mark it as seen&quot; step. Each caller — &lt;code&gt;single_section&lt;/code&gt;, &lt;code&gt;full_build&lt;/code&gt;, and the orphan-page pass — now threads a &lt;code&gt;HashSet&lt;/code&gt; through the section-walking code, so a page reachable through multiple transparent ancestors only ever gets queued, rendered, and copied once.&lt;/p&gt;
&lt;p&gt;It&#39;s a cleaner shape than my version, too — I&#39;d stored the dedup set as a field on the &lt;code&gt;Queue&lt;/code&gt; struct itself; threading it through as a parameter keeps it scoped to a single build pass instead of living on the struct for its whole lifetime.&lt;/p&gt;
&lt;h2 id=&quot;an-hour-of-work-a-fix-a-day-later-a-five-year-old-bug-closed&quot;&gt;An hour of work, a fix a day later, a five-year-old bug closed&lt;/h2&gt;
&lt;p&gt;It&#39;s a good reminder that &quot;stale&quot; and &quot;hard&quot; aren&#39;t the same thing. This one had been open for five years, but once it had a solid investigation, Keats landed the actual fix in about a day. I doesn&#39;t look like the bug was hard to fix — it was hard to &lt;em&gt;see&lt;/em&gt;, because a few things had to line up before it would even show its face:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Windows&lt;/strong&gt;, where concurrent access to the same file path is enforced at the OS level, unlike POSIX systems where it&#39;s usually silently fine&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;transparent&lt;/code&gt; sections&lt;/strong&gt; - i&#39;m guessing possibly a niche feature that not every project in Zola uses&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multiple images or assets on one page&lt;/strong&gt;, since the race window is small and needs several parallel copy jobs racing for the same file to actually collide in practice&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Any one of those alone wouldn&#39;t have reproduced it — which is exactly why &quot;Need Windows help&quot; sat unclaimed for so long despite being a completely deterministic bug once you knew where to look. The fix that mattered was in the queue&#39;s job-dedup logic, not in retrying failed filesystem calls — the retries in my diagnostic commit would have papered over the symptom without addressing why two threads were fighting over the same file in the first place.&lt;/p&gt;
&lt;p&gt;It&#39;s a nice example of how &quot;flaky&quot; bugs are often not flaky at all — they&#39;re deterministic bugs whose triggering conditions are just rare enough that a good repro, not more retries, is what actually finds them.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Gaming Links</title>
        <published>2026-08-02T00:00:00+00:00</published>
        <updated>2026-08-02T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/gaming-links/"/>
        <id>/pages/gaming-links/</id>
        
        <content type="html" xml:base="/pages/gaming-links/"></content>
        
    </entry>
    <entry xml:lang="en">
        <title>Built a Shed From Scratch</title>
        <published>2026-07-26T00:00:00+00:00</published>
        <updated>2026-07-26T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/blog/shed/"/>
        <id>/blog/shed/</id>
        
        <content type="html" xml:base="/blog/shed/">&lt;p&gt;Not every project on here is going to be about computing. This one&#39;s from the garden rather than the terminal — a lean-to style timber shed, built from scratch against the side of the house, from the base up.&lt;/p&gt;
&lt;h2 id=&quot;the-base&quot;&gt;The base&lt;/h2&gt;
&lt;p&gt;A concrete pad was never really on the cards here. The patio underneath is on a noticeable slope, so I went with adjustable paving-slab pedestals instead — they let you level everything out independently of the slope, rather than trying to fight it with the base itself.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/shed/01-base-pedestals-slabs.webp&quot; alt=&quot;Paving slabs set on adjustable pedestals, with a spirit level and the first base timber in place&quot; /&gt;&lt;/p&gt;
&lt;p&gt;With the slabs level, the base frame went together next — treated timber joists with OSB board across the top, again all resting on the pedestals rather than fixed straight to the patio.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/shed/02-base-frame-closeup.webp&quot; alt=&quot;Close-up of the base frame, joists and OSB board, sitting on the levelling pedestals&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/shed/03-base-frame-on-table.webp&quot; alt=&quot;The base frame up on the garden table for the next stage of construction&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;cutting-painting-framing&quot;&gt;Cutting, painting, framing&lt;/h2&gt;
&lt;p&gt;All the timber for this came from &lt;a rel=&quot;external&quot; href=&quot;https://www.aylwardsofcrewshill.co.uk/&quot;&gt;Aylwards Fencing&lt;/a&gt; in Enfield. Worth a mention on its own — the staff there were genuinely helpful, talking through how to actually put the thing together rather than just selling me wood and sending me on my way.&lt;/p&gt;
&lt;p&gt;Next came the least glamorous but most time-consuming part: cutting every piece of timber to length and giving it a coat of red wood stain before assembly — much easier to do flat on the patio than once it&#39;s upright and half-boxed-in.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/shed/04-cutting-and-painting-timber.webp&quot; alt=&quot;Timber lengths clamped to a mitre saw stand, mid-cut, with stain drying on cladding boards nearby&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Once enough pieces were cut and stained, the first wall frame went together on the ground.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/shed/05-wall-frame-assembled.webp&quot; alt=&quot;The first wall frame, assembled flat on the patio with corner brackets fitted&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;getting-the-roof-angle-right&quot;&gt;Getting the roof angle right&lt;/h2&gt;
&lt;p&gt;A lean-to roof means one clean sloped line, which means getting the angle and the height right before anything gets fixed permanently. A fair bit of this stage was just measuring, checking, and measuring again.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/shed/06-measuring-roof-angle-1.webp&quot; alt=&quot;Measuring along the sloped roof beam where it meets the house wall&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;roof-and-cladding&quot;&gt;Roof and cladding&lt;/h2&gt;
&lt;p&gt;With the angle confirmed, the roof structure went on, followed by the cladding boards, working from the bottom up so each row overlaps the one below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/shed/09-roof-panel-on-frame.webp&quot; alt=&quot;The sloped roof panel with slats attached, standing the frame upright&quot; /&gt;
&lt;img src=&quot;/blog/shed/10-back-panel-cladding.webp&quot; alt=&quot;The back panel with cladding boards partially fitted&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The roof beam itself gets fixed directly to the house wall for support:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/shed/11-roof-beam-fixed-to-wall.webp&quot; alt=&quot;The roof beam bolted to the brickwork of the house&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Then the roofing felt went on over the top:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/shed/12-felt-roof-on-frame.webp&quot; alt=&quot;The frame with the grey felt roof covering fitted&quot; /&gt;
&lt;img src=&quot;/blog/shed/13-frame-standing-with-tools.webp&quot; alt=&quot;The shed frame standing upright, felt roof on, tools and offcuts scattered around it&quot; /&gt;
&lt;img src=&quot;/blog/shed/14-interior-frame-detail.webp&quot; alt=&quot;A closer look at the interior framing and shelving supports&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;finished&quot;&gt;Finished&lt;/h2&gt;
&lt;p&gt;A door, hinges, and the last of the cladding later, and it&#39;s a proper shed — narrow enough to fit down the side of the house, tall enough to actually be useful, and built to actually last.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/shed/15-finished-shed-with-door.webp&quot; alt=&quot;The finished shed, doors hung with black hinges, cladding complete&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Total build time was spread over a couple of months of weekends rather than one solid push, and that was less about scheduling than about the fact that this was the first time I&#39;d properly built anything out of wood. A lot of the time went into just thinking — working out the design, second-guessing measurements, walking away from a problem and coming back to it rather than pushing through and getting it wrong.&lt;/p&gt;
&lt;p&gt;And there are things I&#39;d do differently. A few joints aren&#39;t as clean as they should be, the roof angle took more trial and error than it needed to, and there are a couple of decisions I&#39;d make the opposite way with the benefit of hindsight. It&#39;s very much a &quot;V1&quot; — useful, standing, doing its job — but I&#39;ve already mentally sketched out what a V2 would do differently.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Microsoft</title>
        <published>2026-07-24T00:00:00+00:00</published>
        <updated>2026-07-24T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/microsoft/"/>
        <id>/pages/microsoft/</id>
        
        <content type="html" xml:base="/pages/microsoft/"></content>
        
    </entry>
    <entry xml:lang="en">
        <title>Projects</title>
        <published>2026-07-24T00:00:00+00:00</published>
        <updated>2026-07-24T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/projects/"/>
        <id>/pages/projects/</id>
        
        <content type="html" xml:base="/pages/projects/"></content>
        
    </entry>
    <entry xml:lang="en">
        <title>AI Game</title>
        <published>2026-07-23T00:00:00+00:00</published>
        <updated>2026-07-23T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/blog/ai-game/"/>
        <id>/blog/ai-game/</id>
        
        <content type="html" xml:base="/blog/ai-game/">&lt;p&gt;Experiment in asking Claude to code a game&lt;/p&gt;
&lt;p&gt;Plan:  AI &quot;cookie clicker game&quot;&lt;/p&gt;
&lt;p&gt;Plan to implement                                                                                                    │
│                                                                                                                      │
│ Plan: AI Idle Game — Initial Scaffold &amp;amp; MVP                                                                          │
│                                                                                                                      │
│ Context                                                                                                              │
│                                                                                                                      │
│ Building a new browser-based idle game with an AI/tech theme. Core inspiration is Cookie Clicker (clicker loop, big  │
│ numbers, upgrades) crossed with Melvor Idle (skill depth, parallel activities). Unique feature: a hybrid world map   │
│ showing geographic data centre placement with network connection overlays. Boss events are firewall attacks on data  │
│ centres.                                                                                                             │
│                                                                                                                      │
│ ---                                                                                                                  │
│ Tech Stack                                                                                                           │
│                                                                                                                      │
│ ┌─────────────────┬────────────────────┬───────────────────────────────────────────────┐                             │
│ │      Layer      │       Choice       │                    Reason                     │                             │
│ ├─────────────────┼────────────────────┼───────────────────────────────────────────────┤                             │
│ │ Build tool      │ Vite               │ Fast HMR, TS out of the box                   │                             │
│ ├─────────────────┼────────────────────┼───────────────────────────────────────────────┤                             │
│ │ UI              │ React + TypeScript │ Component model fits idle game panels         │                             │
│ ├─────────────────┼────────────────────┼───────────────────────────────────────────────┤                             │
│ │ State           │ Zustand            │ Minimal boilerplate, perfect for game state   │                             │
│ ├─────────────────┼────────────────────┼───────────────────────────────────────────────┤                             │
│ │ Map             │ react-simple-maps  │ Lightweight SVG world map, easy click regions │                             │
│ ├─────────────────┼────────────────────┼───────────────────────────────────────────────┤                             │
│ │ Network overlay │ Custom SVG         │ Lines drawn over the map between DC markers   │                             │
│ ├─────────────────┼────────────────────┼───────────────────────────────────────────────┤                             │
│ │ Styling         │ Tailwind CSS       │ Utility classes, dark sci-fi theme            │                             │
│ ├─────────────────┼────────────────────┼───────────────────────────────────────────────┤                             │
│ │ Animations      │ CSS transitions    │ Keep it lightweight for now                   │                             │
│ └─────────────────┴────────────────────┴───────────────────────────────────────────────┘                             │
│                                                                                                                      │
│ ---                                                                                                                  │
│ Project Structure                                                                                                    │
│                                                                                                                      │
│ E:\aigame\                                                                                                           │
│ ├── src/                                                                                                             │
│ │   ├── components/                                                                                                  │
│ │   │   ├── Map/                                                                                                     │
│ │   │   │   ├── WorldMap.tsx          # react-simple-maps base + SVG overlay                                         │
│ │   │   │   ├── DataCentreMarker.tsx  # Clickable node on map                                                        │
│ │   │   │   └── NetworkLine.tsx       # SVG line between two DC coordinates                                          │
│ │   │   ├── HUD/                                                                                                     │
│ │   │   │   ├── ResourceBar.tsx       # Top bar: Compute, Credits, Bandwidth                                         │
│ │   │   │   └── GameHeader.tsx        # Title + prestige info                                                        │
│ │   │   ├── Panels/                                                                                                  │
│ │   │   │   ├── ClickPanel.tsx        # Main click button + CPS display                                              │
│ │   │   │   ├── UpgradesPanel.tsx     # Purchasable upgrades list                                                    │
│ │   │   │   ├── RegionPanel.tsx       # Sidebar when a region is selected                                            │
│ │   │   │   └── SkillsPanel.tsx       # Melvor-style skill tracks                                                    │
│ │   │   └── Combat/                                                                                                  │
│ │   │       └── BossAlert.tsx         # Overlay when firewall attack starts                                          │
│ │   ├── store/                                                                                                       │
│ │   │   ├── gameStore.ts              # Resources, ticks, CPS                                                        │
│ │   │   ├── mapStore.ts               # Regions, data centres, connections                                           │
│ │   │   └── combatStore.ts            # Active boss events, firewall HP                                              │
│ │   ├── data/                                                                                                        │
│ │   │   ├── regions.ts                # Region definitions (name, coords, bonuses)                                   │
│ │   │   ├── upgrades.ts               # Upgrade tree definitions                                                     │
│ │   │   └── bosses.ts                 # Boss types and stats                                                         │
│ │   ├── hooks/                                                                                                       │
│ │   │   └── useGameLoop.ts            # setInterval tick (100ms), drives all stores                                  │
│ │   ├── App.tsx                       # Layout: map centre, panels left/right                                        │
│ │   ├── main.tsx                                                                                                     │
│ │   └── index.css                     # Tailwind directives + dark theme base                                        │
│ ├── public/                                                                                                          │
│ ├── index.html                                                                                                       │
│ ├── package.json                                                                                                     │
│ ├── vite.config.ts                                                                                                   │
│ ├── tailwind.config.ts                                                                                               │
│ └── tsconfig.json                                                                                                    │
│                                                                                                                      │
│ ---                                                                                                                  │
│ Game Data Model                                                                                                      │
│                                                                                                                      │
│ Resources                                                                                                            │
│                                                                                                                      │
│ { compute: number, credits: number, bandwidth: number, security: number }                                            │
│                                                                                                                      │
│ Region                                                                                                               │
│                                                                                                                      │
│ {                                                                                                                    │
│   id: string, name: string,                                                                                          │
│   coords: [number, number],   // [longitude, latitude] for map projection                                            │
│   unlocked: boolean,                                                                                                 │
│   dataCentreLevel: number,    // 0 = no DC, 1-5 = upgrade levels                                                     │
│   bonusType: &#39;compute&#39; | &#39;bandwidth&#39; | &#39;security&#39;,                                                                   │
│   bonusMultiplier: number                                                                                            │
│ }                                                                                                                    │
│                                                                                                                      │
│ Upgrade                                                                                                              │
│                                                                                                                      │
│ { id: string, name: string, description: string, cost: number, bought: boolean, effect: () =&amp;gt; void }                 │
│                                                                                                                      │
│ Boss Event                                                                                                           │
│                                                                                                                      │
│ { id: string, name: string, targetRegionId: string, firewallHP: number, maxHP: number, active: boolean }             │
│                                                                                                                      │
│ ---                                                                                                                  │
│ Regions (Phase 1 — 6 regions)                                                                                        │
│                                                                                                                      │
│ ┌──────┬───────────────────────┬────────────┬─────────────┐                                                          │
│ │  ID  │         Name          │   Bonus    │ Start state │                                                          │
│ ├──────┼───────────────────────┼────────────┼─────────────┤                                                          │
│ │ na   │ North America         │ +Compute   │ Unlocked    │                                                          │
│ ├──────┼───────────────────────┼────────────┼─────────────┤                                                          │
│ │ eu   │ Europe                │ +Security  │ Locked      │                                                          │
│ ├──────┼───────────────────────┼────────────┼─────────────┤                                                          │
│ │ apac │ Asia Pacific          │ +Bandwidth │ Locked      │                                                          │
│ ├──────┼───────────────────────┼────────────┼─────────────┤                                                          │
│ │ sa   │ South America         │ +Credits   │ Locked      │                                                          │
│ ├──────┼───────────────────────┼────────────┼─────────────┤                                                          │
│ │ mea  │ Middle East &amp;amp; Africa  │ +Security  │ Locked      │                                                          │
│ ├──────┼───────────────────────┼────────────┼─────────────┤                                                          │
│ │ ru   │ Russia / Central Asia │ +Compute   │ Locked      │                                                          │
│ └──────┴───────────────────────┴────────────┴─────────────┘                                                          │
│                                                                                                                      │
│ ---                                                                                                                  │
│ Game Loop (useGameLoop.ts)                                                                                           │
│                                                                                                                      │
│ - Tick every 100ms                                                                                                   │
│ - Each tick: add totalCPS / 10 to compute (since 10 ticks/sec)                                                       │
│ - CPS = sum of all data centre outputs × upgrade multipliers × region bonuses                                        │
│ - Credits generated = compute earned × conversion rate                                                               │
│ - Check for boss spawn conditions on each tick                                                                       │
│                                                                                                                      │
│ ---                                                                                                                  │
│ MVP Scope (Phase 1 — what we build now)                                                                              │
│                                                                                                                      │
│ 1. Project scaffold — Vite + React + TS + Tailwind + Zustand                                                         │
│ 2. Game loop — tick-based resource accumulation                                                                      │
│ 3. Resource bar — displays Compute and Credits with formatted big numbers                                            │
│ 4. Click button — click to earn compute manually (+1 × multiplier per click)                                         │
│ 5. World map — react-simple-maps with 6 regions, North America unlocked                                              │
│ 6. Data centre placement — click unlocked region → build DC (costs Credits)                                          │
│ 7. Network overlay — SVG lines connect regions that both have DCs                                                    │
│ 8. Upgrades panel — 5 starter upgrades (better click, faster DC, etc.)                                               │
│ 9. Region panel — sidebar showing selected region details + upgrade options                                          │
│ 10. Dark sci-fi theme — dark bg, cyan/green accent colours                                                           │
│                                                                                                                      │
│ Phase 2 (planned, not built yet)                                                                                     │
│                                                                                                                      │
│ - Boss / firewall attack events                                                                                      │
│ - Melvor-style skill tracks (Training, Research, Deployment, Security)                                               │
│ - Prestige system                                                                                                    │
│ - Save/load (localStorage)                                                                                           │
│ - Sound effects                                                                                                      │
│                                                                                                                      │
│ ---                                                                                                                  │
│ Implementation Order                                                                                                 │
│                                                                                                                      │
│ 1. npm create vite → install deps (react-simple-maps, zustand, tailwind)                                             │
│ 2. Configure Tailwind + dark theme                                                                                   │
│ 3. gameStore.ts + useGameLoop.ts — core resource tick                                                                │
│ 4. ResourceBar.tsx + ClickPanel.tsx — working clicker                                                                │
│ 5. regions.ts data + mapStore.ts                                                                                     │
│ 6. WorldMap.tsx + DataCentreMarker.tsx                                                                               │
│ 7. NetworkLine.tsx overlay                                                                                           │
│ 8. RegionPanel.tsx sidebar                                                                                           │
│ 9. upgrades.ts + UpgradesPanel.tsx                                                                                   │
│ 10. Wire everything into App.tsx layout                                                                              │
│                                                                                                                      │
│ ---                                                                                                                  │
│ Layout                                                                                                               │
│                                                                                                                      │
│ ┌──────────────────────────────────────────────────────────┐                                                         │
│ │  ResourceBar: [Compute: 1.2M] [Credits: 45K] [BW: 120]  │                                                          │
│ ├──────────────────────────────────────────────────────────┤                                                         │
│ │ ┌─────────────┐ ┌──────────────────────┐ ┌────────────┐ │                                                          │
│ │ │ ClickPanel  │ │   World Map          │ │  Upgrades  │ │                                                          │
│ │ │             │ │   (react-simple-maps)│ │  Panel     │ │                                                          │
│ │ │ [CLICK ME]  │ │   + DC markers       │ │            │ │                                                          │
│ │ │             │ │   + network lines    │ │            │ │                                                          │
│ │ │ CPS: 1,200  │ │                      │ │            │ │                                                          │
│ │ └─────────────┘ └──────────────────────┘ └────────────┘ │                                                          │
│ │                  └── RegionPanel (when region selected) ─┘│                                                        │
│ └──────────────────────────────────────────────────────────┘                                                         │
│                                                                                                                      │
│ ---                                                                                                                  │
│ Verification                                                                                                         │
│                                                                                                                      │
│ - npm run dev opens game in browser at localhost                                                                     │
│ - Clicking the button increases Compute in ResourceBar                                                               │
│ - Idle compute accumulates per tick (verify with no clicking)                                                        │
│ - Map renders world, North America clickable/highlighted                                                             │
│ - Building a DC on NA costs Credits and starts generating Compute                                                    │
│ - Two DCs show a network line between them                                                                           │
│ - Buying an upgrade changes CPS/click value visibly&lt;/p&gt;
&lt;p&gt;First Run through - a bug:&lt;/p&gt;
&lt;p&gt;&quot;I&#39;ve manually got some credits, but the option to build a DC does not enable&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/ai-game/./1.png&quot; alt=&quot;1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Initial design of game&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/ai-game/./initial-design.png&quot; alt=&quot;initial design&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Day 2&lt;/p&gt;
&lt;p&gt;Bosses
Sounds
Save/Load, Offline
Market&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/ai-game/./day-2.png&quot; alt=&quot;day 2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/ai-game/./boss.png&quot; alt=&quot;boss&quot; /&gt;&lt;/p&gt;
&lt;p&gt;what&#39;s Next?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Achievements / Milestones
Passive tracking of lifetime stats (total compute earned, bosses defeated, prestiges done) with badge rewards — Neural Cores or
compute bonuses. Low effort, high feel-good factor.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use the unused resources
bandwidth and security are tracked in the HUD but never spent. They could power:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Bandwidth → unlock inter-region trade routes or speed up building upgrades&lt;/li&gt;
&lt;li&gt;Security → a passive &quot;firewall rating&quot; that reduces boss spawn frequency or dismiss penalty&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;More prestige depth&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;A second prestige layer (&quot;Singularity&quot;) — spend accumulated Neural Cores on a meta-upgrade that persists even through
multiple prestiges&lt;/li&gt;
&lt;li&gt;Prestige challenges (e.g. &quot;no mines run&quot; for bonus cores)&lt;/li&gt;
&lt;li&gt;More shop slots unlocked after N prestiges&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Visual / UX polish&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Floating damage/earn numbers on click and boss hits&lt;/li&gt;
&lt;li&gt;Region unlock/build animations&lt;/li&gt;
&lt;li&gt;Animated Neural Core counter in the header&lt;/li&gt;
&lt;li&gt;Settings panel (reset save, export save string)&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Endgame content&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Boss escalation: bosses get harder each prestige, require strategy to beat&lt;/li&gt;
&lt;li&gt;Region events: random bonuses/penalties on specific regions&lt;/li&gt;
&lt;li&gt;A world-domination win condition (all 20 regions at max buildings)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;Plug immediate gaps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bandwidth/Security spending — right now they pile up forever; giving players something to spend them on (e.g. spend bandwidth
to overclock a region for 60s, spend security to pre-weaken the next boss) would make those resources strategic rather than
passive&lt;/li&gt;
&lt;li&gt;More upgrades — the Upgrades tab has only 5 items and goes empty fast; mid-game needs more rungs on the ladder&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;New progression layer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Research tree — a second tree unlocked after the skill tree; nodes cost bandwidth or security instead of compute, giving
those resources a real sink and adding late-game depth&lt;/li&gt;
&lt;li&gt;Region synergy bonuses — reward having both a DC and a mine in the same region (e.g. +20% mine output if a DC is present),
making region-building a strategic puzzle&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Feel / polish:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Random events — periodic choice cards (not just bosses): &quot;surplus cooling capacity — gain +X bandwidth&quot; or &quot;solar flare —
lose Y compute for 30s, but gain Z security&quot;; adds moment-to-moment interest&lt;/li&gt;
&lt;li&gt;Animated network lines — the DC connection lines on the map are static; pulsing data-flow animations would make the world
feel alive&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My pick: Bandwidth/Security spending + a couple more upgrades. It directly completes the loop opened by the last two features
(achievements + resource wiring), costs are already thematic, and it&#39;s scoped enough to ship cleanly in one pass.&lt;/p&gt;
&lt;p&gt;What sounds right to you?&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Lets give it a new idea:&lt;/p&gt;
&lt;p&gt;&quot;AI is expensive - I&#39;d like a concept where we split credits into chips and money. You can sell compute power for money.
Building a data centre needs credits. Unlocking a region needs money. It&#39;s possible to more money from a loan (but then
game takes interest payments i.e. loan needs to be paid back.&quot;&lt;/p&gt;
&lt;p&gt;Claude:&lt;/p&gt;
&lt;p&gt;&quot;● This is a strong design — it creates real economic tension (sell compute now for expansion vs. keep it for CPS) and makes loans
a meaningful risk. Let me map it out concretely before planning implementation.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Proposed resource split:&lt;/p&gt;
&lt;p&gt;┌──────────────────────────────┬───────────┬────────────────────────┬───────────────────────────────────┐
│           Resource           │  Symbol   │       Earned by        │             Spent on              │
├──────────────────────────────┼───────────┼────────────────────────┼───────────────────────────────────┤
│ Chips ₵ (was Credits)        │ chips     │ 10% of passive compute │ Buildings, upgrades, skills       │
├──────────────────────────────┼───────────┼────────────────────────┼───────────────────────────────────┤
│ Money $                      │ money     │ Selling compute        │ Unlocking regions, loan repayment │
├──────────────────────────────┼───────────┼────────────────────────┼───────────────────────────────────┤
│ Compute, Bandwidth, Security │ unchanged │ unchanged              │ unchanged                         │
└──────────────────────────────┴───────────┴────────────────────────┴───────────────────────────────────┘&lt;/p&gt;
&lt;p&gt;Selling compute:
Manual sell button — converts stored compute to money at a fixed rate (e.g. 1 compute = 0.01 money early game). Selling doesn&#39;t
reduce CPS, just drains the compute pool.&lt;/p&gt;
&lt;p&gt;Loans:
Borrow a lump sum of money now; game deducts interest per second from your money income. Unpaid interest accrues as debt rather
than pushing balance negative. Repay principal + accrued interest manually.
&quot;&lt;/p&gt;
&lt;p&gt;Plan to implement:&lt;/p&gt;
&lt;p&gt;Plan to implement                                                                                                             │
│                                                                                                                               │
│ Plan: Chips / Money Economy Split + Loans                                                                                     │
│                                                                                                                               │
│ Context                                                                                                                       │
│                                                                                                                               │
│ The user wants to split the single &quot;credits&quot; currency into two distinct resources:                                            │
│ - Chips ₵ — earned passively from compute (same as current credits). Used for building infrastructure (DCs, mines, upgrades,  │
│ skills, prestige shop).                                                                                                       │
│ - Money $ — earned by selling compute power. Used exclusively to unlock regions.                                              │
│ - Loans — players can borrow Money from a bank, but pay interest every tick; unpaid debt escalates penalties (higher build    │
│ costs, eventually blocks region unlocking).                                                                                   │
│                                                                                                                               │
│ This adds a meaningful economic tradeoff: spending compute to earn Money locks you out of passive Chips generation            │
│ temporarily, and taking loans is risky if you can&#39;t service the interest.                                                     │
│                                                                                                                               │
│ ---                                                                                                                           │
│ New Files (2)                                                                                                                 │
│                                                                                                                               │
│ src/data/loans.ts                                                                                                             │
│                                                                                                                               │
│ Pure data — zero store imports.                                                                                               │
│ export interface LoanTier {                                                                                                   │
│   id: string;                                                                                                                 │
│   label: string;                                                                                                              │
│   amount: number;       // $ principal                                                                                        │
│   interestRate: number; // fraction per second (applied to remaining balance)                                                 │
│ }                                                                                                                             │
│                                                                                                                               │
│ export const LOAN_TIERS: LoanTier[] = [                                                                                       │
│   { id: &#39;loan_sm&#39;,  label: &#39;Small&#39;,   amount:  500,  interestRate: 0.0002 },                                                  │
│   { id: &#39;loan_md&#39;,  label: &#39;Medium&#39;,  amount: 2000,  interestRate: 0.0003 },                                                  │
│   { id: &#39;loan_lg&#39;,  label: &#39;Large&#39;,   amount: 8000,  interestRate: 0.0004 },                                                  │
│   { id: &#39;loan_xl&#39;,  label: &#39;Massive&#39;, amount: 30000, interestRate: 0.0005 },                                                  │
│ ];                                                                                                                            │
│                                                                                                                               │
│ src/store/loanStore.ts                                                                                                        │
│                                                                                                                               │
│ Zero imports from other game stores — penalties queried via getters by consumers.                                             │
│ interface ActiveLoan { tierId: string; remaining: number; rate: number; }                                                     │
│                                                                                                                               │
│ interface LoanState {                                                                                                         │
│   loans: ActiveLoan[];                                                                                                        │
│   debtAccrued: number;          // total outstanding balance (sum of remaining)                                               │
│   takeLoan: (tier: LoanTier) =&amp;gt; void;                                                                                         │
│   repayLoan: (tierId: string, amount: number) =&amp;gt; void; // calls spendMoney callback                                           │
│   addDebt: (interest: number) =&amp;gt; void;                 // called by gameStore.tick                                            │
│   // Penalty getters                                                                                                          │
│   getBuildCostMultiplier: () =&amp;gt; number; // 1 + debtAccrued / 1000 (build cost penalty)                                        │
│   canUnlockRegions: () =&amp;gt; boolean;      // debtAccrued &amp;lt; 1000                                                                 │
│ }                                                                                                                             │
│ takeLoan(tier) pushes a new ActiveLoan; money added via injected addMoney callback.                                           │
│ repayLoan reduces remaining; removes loan when remaining &amp;lt;= 0.                                                                │
│ addDebt merges new interest into debtAccrued and each loan&#39;s remaining.                                                       │
│                                                                                                                               │
│ ---                                                                                                                           │
│ Modified Files (16)                                                                                                           │
│                                                                                                                               │
│ src/store/gameStore.ts                                                                                                        │
│                                                                                                                               │
│ - Rename resources.credits → resources.chips throughout the interface + state                                                 │
│ - Add resources.money: number (init 0)                                                                                        │
│ - Add autoSellPercent: number (init 0; range 0–50)                                                                            │
│ - Add COMPUTE_SELL_RATE = 0.5 constant (1 compute → $0.50)                                                                    │
│ - Add actions: gainMoney(n), spendMoney(n) → boolean, setAutoSellPercent(n)                                                   │
│ - Add gainChips(n) (rename of gainCredits); keep gainCredits as alias or update all call sites                                │
│ - Modify tick():                                                                                                              │
│   - Auto-sell: computeToSell = totalCPS * deltaSec * autoSellPercent/100; deduct from compute gain, add computeToSell *       │
│ COMPUTE_SELL_RATE to money                                                                                                    │
│   - Interest: loanStore.loans.forEach(l =&amp;gt; interest += l.remaining * l.rate * deltaSec); call loanStore.addDebt(interest)     │
│ then spendMoney(interest) (clamped to available)                                                                              │
│ - Modify buyUpgrade(): read loanStore.getBuildCostMultiplier(), multiply cost before spendChips                               │
│ - Rename spendCredits → spendChips; gainCredits → gainChips                                                                   │
│                                                                                                                               │
│ src/store/mapStore.ts                                                                                                         │
│                                                                                                                               │
│ - buildBuilding(): add loan penalty to costChips = baseCost * loanStore.getBuildCostMultiplier()                              │
│ - upgradeBuilding(): same penalty on upgrade cost                                                                             │
│ - unlockRegion(): read loanStore.canUnlockRegions(); return early (or throw) if blocked                                       │
│                                                                                                                               │
│ src/store/skillStore.ts                                                                                                       │
│                                                                                                                               │
│ - unlockSkill(): spendCompute callback unchanged (skills cost compute not chips) ✓                                            │
│                                                                                                                               │
│ src/store/prestigeStore.ts                                                                                                    │
│                                                                                                                               │
│ - buyPrestigeUpgrade(): spendChips instead of spendCredits                                                                    │
│                                                                                                                               │
│ src/utils/prestige.ts                                                                                                         │
│                                                                                                                               │
│ - doPrestige(): reset resources.chips → 0, resources.money → 0, autoSellPercent → 0; loans survive (debt intentionally        │
│ persists across prestige as punishment)                                                                                       │
│                                                                                                                               │
│ src/utils/persist.ts                                                                                                          │
│                                                                                                                               │
│ - SaveData.game.resources: change credits → chips; add money                                                                  │
│ - Add loanStore save/restore: loans: ActiveLoan[], debtAccrued: number                                                        │
│ - Migration: chips = save.game.resources.chips ?? (save.game.resources as any).credits ?? 0                                   │
│ - Offline earnings: compute auto-sell offline (same autoSellPercent applied to offline compute)                               │
│                                                                                                                               │
│ src/data/upgrades.ts                                                                                                          │
│                                                                                                                               │
│ - Upgrades already cost credits → rename field label to &quot;chips&quot; in description strings only; cost values unchanged            │
│                                                                                                                               │
│ src/components/Panels/RightPanel.tsx                                                                                          │
│                                                                                                                               │
│ - No change needed (market tab already exists as &#39;market&#39;)                                                                    │
│                                                                                                                               │
│ src/components/Panels/MarketPanel.tsx                                                                                         │
│                                                                                                                               │
│ Full overhaul — 3 inner tabs: MTRL | SELL | BANK                                                                              │
│                                                                                                                               │
│ MTRL tab (existing material buy/craft content, unchanged logic)                                                               │
│                                                                                                                               │
│ SELL tab (new):                                                                                                               │
│ AUTO-SELL COMPUTE                                                                                                             │
│ [slider 0%──────50%] {value}%                                                                                                 │
│ Selling {x} compute/s → +${y}/s                                                                                               │
│                                                                                                                               │
│ MANUAL SELL                                                                                                                   │
│ [SELL 100]  [SELL 1K]  [SELL ALL]                                                                                             │
│ - Slider updates setAutoSellPercent                                                                                           │
│ - Manual sell: spendCompute(n) + gainMoney(n * COMPUTE_SELL_RATE)                                                             │
│                                                                                                                               │
│ BANK tab (new):                                                                                                               │
│ MONEY: $12,450                                                                                                                │
│ DEBT PENALTY: +23% build cost  [warning if debtAccrued &amp;gt; 0]                                                                   │
│ REGION LOCK: ACTIVE            [warning if canUnlockRegions() = false]                                                        │
│                                                                                                                               │
│ ACTIVE LOANS                                                                                                                  │
│   Small   $480 remaining  [REPAY $100] [REPAY ALL]                                                                            │
│                                                                                                                               │
│ TAKE A LOAN                                                                                                                   │
│   [Small $500]  [Medium $2,000]  [Large $8,000]  [Massive $30,000]                                                            │
│                                                                                                                               │
│ src/components/HUD/ResourceBar.tsx                                                                                            │
│                                                                                                                               │
│ - Replace &quot;CREDITS&quot; item → &quot;CHIPS ₵&quot; showing resources.chips                                                                  │
│ - Add &quot;MONEY $&quot; item showing resources.money with optional +$/s from auto-sell rate                                           │
│                                                                                                                               │
│ src/components/UI/OfflineModal.tsx                                                                                            │
│                                                                                                                               │
│ - Rename &quot;CREDITS&quot; label → &quot;CHIPS&quot;                                                                                            │
│ - Add &quot;MONEY&quot; row if offline money &amp;gt; 0.01                                                                                     │
│                                                                                                                               │
│ src/components/Panels/AchievementsPanel.tsx + src/hooks/useAchievementChecker.ts                                              │
│                                                                                                                               │
│ - Update totalCreditsEarned → totalChipsEarned in AchievementContext if renaming that field (or keep as-is since it tracks    │
│ total chips earned)                                                                                                           │
│                                                                                                                               │
│ src/components/Combat/BossAlert.tsx                                                                                           │
│                                                                                                                               │
│ - gainCredits → gainChips for boss reward                                                                                     │
│                                                                                                                               │
│ src/store/achievementStore.ts                                                                                                 │
│                                                                                                                               │
│ - Achievement rewards paying &quot;credits&quot; type → now pay chips: no interface change needed (reward callback is a closure)        │
│                                                                                                                               │
│ src/index.css                                                                                                                 │
│                                                                                                                               │
│ - Add .bank-tab styles, .loan-card, .sell-tab, auto-sell slider styles                                                        │
│ - .resource-item for money (green #22c55e accent to distinguish from cyan chips)                                              │
│                                                                                                                               │
│ ---                                                                                                                           │
│ Rename Summary                                                                                                                │
│                                                                                                                               │
│ ┌─────────────────────┬──────────────────┐                                                                                    │
│ │         Old         │       New        │                                                                                    │
│ ├─────────────────────┼──────────────────┤                                                                                    │
│ │ resources.credits   │ resources.chips  │                                                                                    │
│ ├─────────────────────┼──────────────────┤                                                                                    │
│ │ spendCredits()      │ spendChips()     │                                                                                    │
│ ├─────────────────────┼──────────────────┤                                                                                    │
│ │ gainCredits()       │ gainChips()      │                                                                                    │
│ ├─────────────────────┼──────────────────┤                                                                                    │
│ │ totalCreditsEarned  │ totalChipsEarned │                                                                                    │
│ ├─────────────────────┼──────────────────┤                                                                                    │
│ │ 💳 CREDITS UI label │ ₵ CHIPS          │                                                                                    │
│ └─────────────────────┴──────────────────┘                                                                                    │
│                                                                                                                               │
│ ---                                                                                                                           │
│ Circular Dependency Check                                                                                                     │
│                                                                                                                               │
│ - loans.ts → nothing ✓                                                                                                        │
│ - loanStore.ts → loans.ts only ✓                                                                                              │
│ - gameStore.ts → reads loanStore.getState() (same pattern as mapStore) ✓                                                      │
│ - mapStore.ts → reads loanStore.getState() ✓                                                                                  │
│ - persist.ts → adds loanStore import (same pattern) ✓                                                                         │
│                                                                                                                               │
│ ---                                                                                                                           │
│ Verification                                                                                                                  │
│                                                                                                                               │
│ 1. npx tsc --noEmit — zero errors                                                                                             │
│ 2. Start fresh → ResourceBar shows CHIPS ₵ and MONEY $ (both 0)                                                               │
│ 3. Build a DC → chips decrease; money unchanged                                                                               │
│ 4. SELL tab → drag auto-sell to 10% → money ticks up; compute gain slightly reduced                                           │
│ 5. SELL tab → click SELL ALL → compute drops to 0, money increases                                                            │
│ 6. BANK tab → take Small $500 loan → money +500, loan appears in active list                                                  │
│ 7. Watch interest tick: money slowly decreases; BANK tab shows remaining balance                                              │
│ 8. Build cost penalty: at $500 debt accrued, building costs ~50% more                                                         │
│ 9. At $1000 debt accrued, unlock region button is greyed out / shows warning                                                  │
│ 10. Repay loan → debt drops → penalties reduce                                                                                │
│ 11. Save &amp;amp; reload → chips, money, loans all persist correctly                                                                 │
│ 12. Prestige → chips/money reset to 0; loans/debt survive&lt;/p&gt;
&lt;p&gt;Day 3&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/ai-game/./claud-1.png&quot; alt=&quot;claud 1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/ai-game/./day-3.png&quot; alt=&quot;day 3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;So,  next step, ask the  AI where we are:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/ai-game/./day-3-status.png&quot; alt=&quot;day 3 status&quot; /&gt;&lt;/p&gt;
&lt;p&gt;leaderboard/cloud save&lt;/p&gt;
&lt;p&gt;Boss:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Map takeover                 ┌────────────────────────────────────────────┐&lt;/li&gt;
&lt;li&gt;Persistent siege             │ ╔══════════════════════════════════════╗   │&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Network intercept            │ ║  NETWORK INTERCEPT — APT-X9          ║   │
│ ║  You have 20s to respond             ║   │
A terminal-style panel slides     │ ╠══════════════════════════════════════╣   │
up from the bottom. You&#39;re        │ ║  &amp;gt; [FLOOD ROUTE]   cost: 800 BW      ║   │
given a sequence of 3–5 action    │ ║  &amp;gt; [SPOOF ID]      cost: 500 SEC     ║   │
choices (spend resources)        │ ║  &amp;gt; [HARD DROP]     cost: 1200 chips  ║   │
within a time window to           │ ║  &amp;gt; [IGNORE]        penalty: -15% CPS ║   │
neutralise the threat. Quick      │ ╚══════════════════════════════════════╝   │
and decisive, over in 20s.        │   Slides up from bottom, resolves fast,    │
│   no screen block.                         │
└────────────────────────────────────────────┘&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Persistent siege             │ ┌────────────────────────────────────────┐ │&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Network intercept            │ │ ⚠ SIEGE ACTIVE: Botnet Swarm          │  │
│ │ Draining 2% chips/s │ 47s remaining   │  │
Boss appears as a                 │ │ [RESIST: click to absorb packets]      │ │
banner/ticker — doesn&#39;t block     │ └────────────────────────────────────────┘ │
gameplay. It slowly corrupts      │                                            │
your resources over time. You     │   Left panel gets a resistance meter       │
defeat it by clicking a           │   instead of a full-screen modal.          │
counter in the left panel         │   Game continues completely normally.      │
while continuing to build         └────────────────────────────────────────────┘
normally.
Notes: press n to add notes&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Map takeover                 ┌────────────────────────────────────────────┐&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Persistent siege             │ ┌─────────────────────────────────┐        │&lt;/li&gt;
&lt;li&gt;Network intercept            │ │  WORLD MAP                      │        │
│ │                                 │        │
The boss infects a region — it    │ │   [US] [CA]  [⚠ CN INFECTED]   │         │
turns hostile on the map, its    │ │              ^^^^ red glow      │        │
DCs go offline, and you          │ │         [JP] [AU]               │        │
reclaim it through the            │ │                                 │        │
RegionPanel while the rest of     │ │  ┌── INFECTED REGION ──────┐   │         │
the game keeps running. No        │ │  │ ⚠ Exfiltration in 60s   │   │         │
full-screen block.                │ │  │ [PURGE — 500 SEC]       │   │         │
│ │  │ [ISOLATE — 300 BW]      │   │         │
│ │  └─────────────────────────┘   │         │
│ └─────────────────────────────────┘        │
└────────────────────────────────────────────┘&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Day 4:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More content — upgrades ladder only goes to 7M chips, late game could use more; only 6 prestige shop items&lt;/li&gt;
&lt;li&gt;Win/endgame clarity — singularity exists but no clear &quot;you won&quot; moment or narrative arc&lt;/li&gt;
&lt;li&gt;Stats/run history — no way to see what happened in a run (bosses defeated, events resolved, etc.)&lt;/li&gt;
&lt;li&gt;Achievements for new systems — Botnet/Intercept/Exfil bosses have no achievements yet&lt;/li&gt;
&lt;li&gt;Polish — boss sounds for the 3 new boss types are missing (they use generic spawn sound)&lt;/li&gt;
&lt;li&gt;Balance — new bosses haven&#39;t been tuned relative to each other&#39;s spawn rates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Day 5:&lt;/p&gt;
&lt;p&gt;● Honest assessment: the core loop is feature-complete — there&#39;s a clear start, mid-game, late-game, and win condition.
But &quot;feature-complete&quot; and &quot;finished&quot; aren&#39;t the same thing.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;What&#39;s solid&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full progression arc: click → DCs → regions → prestige → singularity → ascension&lt;/li&gt;
&lt;li&gt;6 distinct boss mechanics, each with unique UI&lt;/li&gt;
&lt;li&gt;Materials, crafting, research, loans, skills — real depth at every tier&lt;/li&gt;
&lt;li&gt;Save/load, offline earnings, achievements, run stats&lt;/li&gt;
&lt;li&gt;Win condition with narrative arc&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;Where it still has gaps&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Player guidance (biggest gap)
The tutorial covers the first 5 minutes. Research, forge, loan system, boss mechanics, region synergy — none of these
are ever explained. A new player hitting &quot;REGION INFECTED&quot; for the first time has no idea what to do.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Balance is unverified
The upgrade ladder, prestige core formula, and singularity threshold have never been playtested end-to-end. The
mid-to-late game curve (50k → 7M chips before the new upgrades) might feel like a wall or a breeze — impossible to
know without real sessions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bandwidth and security feel inert
They generate passively and get spent in combat (security) or burst mode (bandwidth) — but a player can fully win
ignoring both. They need either more sinks or more visible rewards so players actually care about them.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The boss videos folder is empty
public/videos/ is referenced in BossAlert for DDoS/Ransomware/APT but presumably has nothing in it, so those bosses
show a black video panel.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Singularity shop is thin
6 items vs 12 prestige shop items and 20 skill/research nodes. The singularity layer — which should feel most
impactful — has the least to buy.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Day 6:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/ai-game/./day-6.png&quot; alt=&quot;day 6&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/ai-game/./6-2.png&quot; alt=&quot;6 2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Day 7&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/ai-game/./day-7.png&quot; alt=&quot;day 7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Bugs (actual correctness issues)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;💳 per click is wrong — chipGain = gain * 1 in click() means clicking gives chips at a 1:1 rate, but passive
generation is 6% of compute. Either the rate is intentional (and the tooltip I just added is wrong) or gain * 1 should
be gain * 0.06. Worth deciding and fixing.&lt;/li&gt;
&lt;li&gt;L7–10 colours missing in RegionPanel.tsx — we updated BuildingsListView.tsx but RegionPanel.tsx has its own
duplicate colour maps that still only go to L6, so L7–10 buildings render with the fallback colour there.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;UX / Clarity&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;No tooltip on &quot;💳 CHIPS&quot; vs &quot;$ MONEY&quot; distinction — new players don&#39;t know chips come from compute-to-chips
conversion and money comes from auto-selling compute. The resource bar already has tooltips but this distinction is
often the first stumbling block.&lt;/li&gt;
&lt;li&gt;Prestige tier card chain isn&#39;t obvious visually — Tier II is greyed out as &quot;Requires Tier I&quot; but there&#39;s no visual
arrow or numbering to show the progression sequence at a glance.&lt;/li&gt;
&lt;li&gt;&quot;MOVE HQ HERE&quot; button has no cost/consequence explanation — players don&#39;t know it&#39;s free and instant, or that it
removes upkeep.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;Balance / Progression feel&lt;/p&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;💳 per click = ⚡ per click is very generous early — if intentional, it makes chips trivially easy to earn by
clicking, which may short-circuit the early game economy.&lt;/li&gt;
&lt;li&gt;Research unlock gate (prestige ≥ 1 AND 20 skills bought) — 20 skills costs a huge amount of compute. May be hitting
this gate well after it feels relevant.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;Minor visual polish&lt;/p&gt;
&lt;ol start=&quot;8&quot;&gt;
&lt;li&gt;Skill node connector lines look like plain borders — adding a subtle arrow or clearer directionality would help
players read the dependency tree.&lt;/li&gt;
&lt;li&gt;Achievements panel category counts — showing &quot;3/5&quot; unlocked per category tab would let players know where to focus.&lt;/li&gt;
&lt;li&gt;No visual feedback when you can&#39;t afford a prestige upgrade — the card dims but doesn&#39;t tell you how many more
cores you need. A &quot;need X more ◈&quot; line would help.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;Highest-priority fixes: #2 (actual visual bug from our L7-10 work), #1 (chips-per-click ambiguity), and #10 (prestige
&quot;need X cores&quot; feedback) would have the most immediate impact. Want to start with those?&lt;/p&gt;
&lt;p&gt;Resume this session with:
claude --resume 65f42417-0b67-4c42-b115-a6367e431c17&lt;/p&gt;
&lt;p&gt;18th  March -&lt;/p&gt;
&lt;p&gt;● The game is pretty feature-complete at this point. Here are some directions that would have the most impact:&lt;/p&gt;
&lt;p&gt;Polish / Feel&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More visual feedback on clicks, boss defeats, prestige&lt;/li&gt;
&lt;li&gt;Animated numbers (floating +CPS on tick)&lt;/li&gt;
&lt;li&gt;Better mobile layout&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Content&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More upgrade tiers (currently only 5 starters)&lt;/li&gt;
&lt;li&gt;New boss mechanics or variants&lt;/li&gt;
&lt;li&gt;More random events (only 12 now)&lt;/li&gt;
&lt;li&gt;Additional research nodes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Depth / Endgame&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A proper win condition / ending screen&lt;/li&gt;
&lt;li&gt;More singularity shop upgrades&lt;/li&gt;
&lt;li&gt;Cross-run meta progression beyond singularity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;QoL&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Statistics panel (lifetime totals, rates, graphs)&lt;/li&gt;
&lt;li&gt;Hotkeys&lt;/li&gt;
&lt;li&gt;Export/import save&lt;/li&gt;
&lt;li&gt;Settings panel (speed, volume, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Technical&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloud saves&lt;/li&gt;
&lt;li&gt;Performance audit (the 100ms tick + multiple stores)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What&#39;s been feeling thin or missing when you play it?&lt;/p&gt;
&lt;p&gt;So  when  challenged thatsome of the above alrerdy exists:&lt;/p&gt;
&lt;p&gt;● Yeah, quite a bit already exists — StatsPanel, SettingsPanel, AscensionModal, DebugPanel. My list was sloppy.&lt;/p&gt;
&lt;p&gt;Let me look at what&#39;s actually thin:&lt;/p&gt;
&lt;p&gt;● Read 2 files (ctrl+o to expand)&lt;/p&gt;
&lt;p&gt;● Yeah, much more exists than I said. The game is genuinely well-fleshed out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upgrades: 33 upgrades spanning early to endgame (1B chips) — solid&lt;/li&gt;
&lt;li&gt;Settings: music toggle/volume, export/import save, hard reset — all there&lt;/li&gt;
&lt;li&gt;Components: Stats panel, Ascension modal, Debug panel already exist&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Genuine gaps I can see:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;No win/end state — AscensionModal exists but unclear if it&#39;s wired up to anything&lt;/li&gt;
&lt;li&gt;StatsPanel content — worth checking what&#39;s actually in it&lt;/li&gt;
&lt;li&gt;Balance — given the recent balance passes, probably the most impactful thing is playtesting to find where the
pacing breaks&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Want me to check what&#39;s in StatsPanel and AscensionModal to see if those are actually finished?&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Archiving a 2014 Fix: App-V, wisptis.exe, and a Wacom Mouse Lag Bug</title>
        <published>2026-07-23T00:00:00+00:00</published>
        <updated>2026-07-23T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/blog/appv-wisptis/"/>
        <id>/blog/appv-wisptis/</id>
        
        <content type="html" xml:base="/blog/appv-wisptis/">&lt;p&gt;Part of keeping a record of past work is admitting that not all of it lives on your own site. Some of it ends up documented elsewhere, credited in someone else&#39;s write-up rather than your own. This is one of those.&lt;/p&gt;
&lt;p&gt;Back in 2014, a Microsoft MVP running the blog &lt;a rel=&quot;external&quot; href=&quot;https://www.applepie.se/app-v-and-wisptis-exe&quot;&gt;applepie.se&lt;/a&gt; wrote up a nasty App-V 5 issue we&#39;d been chasing: applications virtualized with App-V that used .NET Framework 4.5.2 (WPF, Silverlight, the SCOM/SCCM console, the App-V UI itself) would trigger severe mouse lag or a full hang whenever a tablet-input-capable device — most commonly a Wacom tablet — was connected. The culprit was &lt;code&gt;wisptis.exe&lt;/code&gt;, the process responsible for handling pen input; killing it restored normal mouse behaviour immediately, which was the first clue.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/appv-wisptis/01-twitter-report.png&quot; alt=&quot;The original report, spotted on Twitter&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The write-up is credited to the site&#39;s author, but the underlying diagnosis and several of the fixes trace back to troubleshooting I&#39;d done at the time — it&#39;s referenced throughout as &quot;thanks to Paul Richards&quot; for both the initial diagnosis and a couple of the workarounds. Just happened - he got asked on Twitter by someone else as we&#39;d been troubleshooting the issue. It&#39;s a good example of the kind of unglamorous, deep-in-the-registry sysadmin work that doesn&#39;t often get written up publicly, so it&#39;s worth keeping a pointer to it here as part of the record — alongside the &lt;a href=&quot;/blog/website/&quot;&gt;website and intranet history&lt;/a&gt; already on this site.&lt;/p&gt;
&lt;p&gt;For anyone landing here from a search for the same issue, the short version:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Symptom&lt;/strong&gt;: mouse freezes when a WPF/.NET 4.5.2 app is launched inside an App-V 5 bubble, on a machine with a tablet-input device attached.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root cause&lt;/strong&gt;: App-V&#39;s handling of the process meant &lt;code&gt;wisptis.exe&lt;/code&gt; failed to start correctly (&lt;code&gt;STATUS_ELEVATION_REQUIRED&lt;/code&gt;), and the resulting stuck state locked up the mouse.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workarounds explored&lt;/strong&gt;: reverting to .NET 4.5.1, disabling the Tablet PC input service outright, overriding a specific WPF interface registry key inside the App-V package at sequencing time, and — the one Microsoft eventually confirmed — adding an &lt;code&gt;ObjExclusions&lt;/code&gt; registry entry on the App-V client so the process wasn&#39;t tracked the way that triggered the hang.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long-term fix&lt;/strong&gt;: rolled into App-V 5.0 SP3.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The blunter workarounds, for reference — disabling tablet input via Group Policy:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/appv-wisptis/02-tablet-pc-cursor-policy.png&quot; alt=&quot;Group Policy: User Configuration → Administrative Templates → Windows Components → Tablet PC → Cursor&quot; /&gt;&lt;/p&gt;
&lt;p&gt;...or disabling the underlying service outright:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/appv-wisptis/03-touch-keyboard-service.png&quot; alt=&quot;The Touch Keyboard and Handwriting Panel Service&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And the more surgical fix — overriding the WPF tablet-detection interface inside the App-V package at sequencing time, scoped to just the virtualized app:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/appv-wisptis/04-registry-interface-override.png&quot; alt=&quot;Registry override for the WPF stylus/tablet interface, set to Override Local&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The full detail, including the exact registry paths and the community follow-up from other admins who hit the same thing on App-V 4.6, is on the &lt;a rel=&quot;external&quot; href=&quot;https://www.applepie.se/app-v-and-wisptis-exe&quot;&gt;original post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It&#39;s a small thing to archive, but it&#39;s a fair snapshot of what a lot of education-sector IT work actually looked like a decade ago: less &quot;modern web stack,&quot; more registry keys and process traces at midnight.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Making sense of Arbor&#39;s GraphQL API</title>
        <published>2026-07-23T00:00:00+00:00</published>
        <updated>2026-07-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            Paul Richards
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="/blog/arbor-calendar-sync-arbor-graphql/"/>
        <id>/blog/arbor-calendar-sync-arbor-graphql/</id>
        
        <content type="html" xml:base="/blog/arbor-calendar-sync-arbor-graphql/">&lt;h1 id=&quot;making-sense-of-arbor-s-graphql-api&quot;&gt;Making sense of Arbor&#39;s GraphQL API&lt;/h1&gt;
&lt;p&gt;Post 1 set out the idea: pull the desired state of every staff member&#39;s timetable
out of Arbor, once per run, and hand it to a reconciliation stage that writes it to
Outlook. That first half — &quot;pull the desired state out of Arbor&quot; — turned out to be
most of the work. Arbor exposes a GraphQL API, and GraphQL&#39;s self-describing schema
makes it feel like you can just ask for what you want. Arbor does publish API
documentation and some example queries, and it&#39;s a reasonable starting point — but
it doesn&#39;t go deep enough to get you to a working sync. In practice we hit five
separate quirks before the query returned what we actually needed, and none of them
were covered by the docs or examples. This post is those five things.&lt;/p&gt;
&lt;h2 id=&quot;1-calendars-have-no-owner-filter-so-you-can-t-just-ask-give-me-this-staff-member-s-events&quot;&gt;1. Calendars have no owner filter — so you can&#39;t just ask &quot;give me this staff member&#39;s events&quot;&lt;/h2&gt;
&lt;p&gt;The obvious query is something like &quot;give me Staff X&#39;s calendar entries.&quot; Arbor
doesn&#39;t have that. &lt;code&gt;Calendar&lt;/code&gt; has no filter by owner, and the only way to reach a
staff member&#39;s calendars at all is a &lt;em&gt;nested&lt;/em&gt; field: &lt;code&gt;Staff.calendarEntryMappings&lt;/code&gt;.
Every staff member owns two calendars — &lt;code&gt;School&lt;/code&gt; and &lt;code&gt;My Lessons&lt;/code&gt; — and this nested
field is the union of both.&lt;/p&gt;
&lt;p&gt;So calendar-id discovery becomes its own step, separate from fetching events:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;graphql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-8&quot;&gt; Staff&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;id_in&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;123&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 456&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-8&quot;&gt; staffId&lt;/span&gt;&lt;span class=&quot;z-l-8&quot;&gt; calendarEntryMappings&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-8&quot;&gt; calendar&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-8&quot;&gt; id&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Harvest the calendar ids per staff member, then fetch actual events by calendar id,
not staff id.&lt;/p&gt;
&lt;h2 id=&quot;2-that-same-nested-field-only-sees-the-current-academic-year&quot;&gt;2. That same nested field only sees the current academic year&lt;/h2&gt;
&lt;p&gt;Here&#39;s the field-level gotcha that cost us the most time: &lt;code&gt;Staff.calendarEntryMappings&lt;/code&gt;
takes no date arguments, and it only ever returns entries from the current academic
year. In July, staff whose September timetable is already published in Arbor show
up with no autumn-term entries via this field at all.&lt;/p&gt;
&lt;p&gt;The fix is that Arbor also exposes &lt;code&gt;CalendarEntryMapping&lt;/code&gt; as a &lt;strong&gt;top-level&lt;/strong&gt; query,
and that version takes real filters:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;graphql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8&quot;&gt;  CalendarEntryMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;    calendar__id_in&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;123&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 456&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;    startDatetime_after_or_equal&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;2026-09-01 00:00:00&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;    startDatetime_before&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;2026-09-08 00:00:00&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;    page_size&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 200&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;    page_num&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  )&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8&quot;&gt;    startDatetime&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8&quot;&gt;    endDatetime&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8&quot;&gt;    eventDisplayName&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8&quot;&gt;    calendar&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-8&quot;&gt; id&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8&quot;&gt;    event&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-8&quot;&gt; __typename&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This one respects date ranges and happily returns next year&#39;s published
timetable. So the nested field is only used for step 1 (discovering calendar ids);
every actual event comes from the top-level query. Two different fields, same
underlying data, very different date behaviour — nothing in the schema signals
this, you find it by trying both.&lt;/p&gt;
&lt;h2 id=&quot;3-current-staff-doesn-t-mean-what-you-d-think&quot;&gt;3. &quot;Current staff&quot; doesn&#39;t mean what you&#39;d think&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Staff(currentStaff: true)&lt;/code&gt; sounds like the right filter for &quot;who do we sync.&quot; It
isn&#39;t complete: staff who&#39;ve been entered into Arbor ahead of their start date
(new September hires added over the summer) are neither &lt;code&gt;currentStaff: true&lt;/code&gt; nor
&lt;code&gt;false&lt;/code&gt; — they&#39;re excluded from both. There&#39;s no server-side filter on
&lt;code&gt;joiningDate&lt;/code&gt;/&lt;code&gt;leavingDate&lt;/code&gt; either.&lt;/p&gt;
&lt;p&gt;The workaround is a second, broader pass: page through &lt;code&gt;externalStaff: false&lt;/code&gt; (the
widest catch-all we found), and client-side keep anyone whose &lt;code&gt;joiningDate&lt;/code&gt; falls
on or before the end of the sync window and who has no &lt;code&gt;leavingDate&lt;/code&gt; — merged in
with the &lt;code&gt;currentStaff: true&lt;/code&gt; set. Two full paginated queries just to get a
correct staff list, because there&#39;s no single filter that means &quot;will be relevant
during this sync window.&quot;&lt;/p&gt;
&lt;h2 id=&quot;4-new-starters-break-calendar-id-discovery-too-which-needs-its-own-fallback&quot;&gt;4. New starters break calendar-id discovery too — which needs its own fallback&lt;/h2&gt;
&lt;p&gt;Combine points 1 and 3: a staff member who joins in September has no current-year
calendar entries yet, so the nested &lt;code&gt;Staff.calendarEntryMappings&lt;/code&gt; field (our only
route to their calendar ids) returns nothing for them. They&#39;re invisible to
discovery even though they&#39;re correctly in the staff list.&lt;/p&gt;
&lt;p&gt;There&#39;s no staff filter on the top-level &lt;code&gt;CalendarEntryMapping&lt;/code&gt; query either, so we
can&#39;t just ask it directly. The fallback: page through the &lt;strong&gt;unfiltered&lt;/strong&gt; top-level
query over a short, bounded window — about two weeks from each missing staff
member&#39;s joining date — and attribute rows back to a person via the &lt;code&gt;mapped&lt;/code&gt; union
field, which resolves to &lt;code&gt;Staff { staffId }&lt;/code&gt; when a row belongs to a calendar owned
by staff:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;graphql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8&quot;&gt;  CalendarEntryMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;    startDatetime_after_or_equal&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;2026-09-01 00:00:00&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;    startDatetime_before&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;2026-09-15 00:00:00&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;    page_size&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 200&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;    page_num&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  )&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8&quot;&gt;    calendar&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-8&quot;&gt; id&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8&quot;&gt;    mapped&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-8&quot;&gt; __typename&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; ...&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; on&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-1&quot;&gt; Staff&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-8&quot;&gt; staffId&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This only works because it&#39;s bounded to ~2 weeks per missing person; unfiltered
over a whole term would mean paging through a school&#39;s entire calendar (rooms,
sessions, everything) just to find a handful of new starters. Anyone still
unresolved after the probe gets reported on the console rather than silently
dropped.&lt;/p&gt;
&lt;h2 id=&quot;5-a-400-response-isn-t-necessarily-a-failure&quot;&gt;5. A 400 response isn&#39;t necessarily a failure&lt;/h2&gt;
&lt;p&gt;Arbor returns field-level &lt;strong&gt;authorization&lt;/strong&gt; errors — HTTP 400, GraphQL &lt;code&gt;errors&lt;/code&gt;
array populated — whenever the event union in a query resolves to a type the API
credential isn&#39;t allowed to read (e.g. &lt;code&gt;StaffAbsence&lt;/code&gt; details a timetable-sync
account has no business seeing). Critically, the response still carries the rest
of the data: title and times for the denied event, just no location.&lt;/p&gt;
&lt;p&gt;Treating any non-2xx or any populated &lt;code&gt;errors&lt;/code&gt; array as fatal would mean the whole
sync throws on the first restricted event type. Instead:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;nonAuth&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; @&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;errors&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; |&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-14&quot;&gt; Where-Object&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; $&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt;_&lt;/span&gt;&lt;span&gt;.extensions.category&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -ne&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt; &amp;#39;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;authorization&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;.data&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -and&lt;/span&gt;&lt;span&gt; $&lt;/span&gt;&lt;span&gt;nonAuth&lt;/span&gt;&lt;span&gt;.Count&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -eq&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;  #&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; only authorization errors, and we still got data - summarise and continue&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anything that isn&#39;t an &lt;code&gt;authorization&lt;/code&gt;-category error is still treated as fatal.
&lt;code&gt;429&lt;/code&gt;/&lt;code&gt;500&lt;/code&gt;/&lt;code&gt;502&lt;/code&gt;/&lt;code&gt;503&lt;/code&gt;/&lt;code&gt;504&lt;/code&gt; get a separate path: exponential backoff and retry, up
to four attempts, before giving up.&lt;/p&gt;
&lt;h2 id=&quot;the-smaller-stuff&quot;&gt;The smaller stuff&lt;/h2&gt;
&lt;p&gt;A few more things that only surface once you&#39;re actually calling the API:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;page_num&lt;/code&gt; is &lt;strong&gt;0-based&lt;/strong&gt;, unlike the 1-based pagination you&#39;d instinctively
reach for.&lt;/li&gt;
&lt;li&gt;The free tier caps &lt;code&gt;page_size&lt;/code&gt; at 500 and rate-limits at 100 requests per school
per minute — we track a sliding 60-second window client-side and throttle at 90,
leaving headroom for retries and anything else hitting the same school.&lt;/li&gt;
&lt;li&gt;Not every event type in the union exposes &lt;code&gt;locationDisplayName&lt;/code&gt; — figuring out
which ones do meant introspecting the schema, since there&#39;s no field list to
read anywhere.&lt;/li&gt;
&lt;li&gt;Every raw request and response gets dumped to a debug folder, wiped and rebuilt
each run. Once we hit our second or third undocumented edge case, &quot;read the last
run&#39;s actual wire traffic&quot; became faster than guessing from the schema.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this is exotic — it&#39;s the ordinary experience of integrating with a GraphQL
API that has a schema and some docs, but no guide for the specific query shapes a
given use case needs. The schema and the docs tell you what&#39;s &lt;em&gt;possible&lt;/em&gt;; neither
tells you that the field you&#39;d naturally reach for only covers the current academic
year, or that &quot;current&quot; staff excludes people who are, for every practical purpose,
current.&lt;/p&gt;
&lt;p&gt;Next up: post 3, on the other side of the pipeline — how the reconciliation engine
turns this data into safe, idempotent writes against Microsoft Graph.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Building a calendar sync you can trust to run unattended</title>
        <published>2026-07-23T00:00:00+00:00</published>
        <updated>2026-07-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            Paul Richards
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="/blog/arbor-calendar-sync-reconciliation-engine/"/>
        <id>/blog/arbor-calendar-sync-reconciliation-engine/</id>
        
        <content type="html" xml:base="/blog/arbor-calendar-sync-reconciliation-engine/">&lt;h1 id=&quot;building-a-calendar-sync-you-can-trust-to-run-unattended&quot;&gt;Building a calendar sync you can trust to run unattended&lt;/h1&gt;
&lt;p&gt;Post 1 set the bar for this tool: it has to be safe to run on a schedule, forever,
without anyone watching it. Post 2 covered getting the data out of Arbor. This post
is stage 2 — &lt;code&gt;Sync-OutlookCalendars.ps1&lt;/code&gt; and the &lt;code&gt;TimetableSync&lt;/code&gt; module — turning
&lt;code&gt;events.json&lt;/code&gt; into writes against Microsoft Graph without ever touching an event it
doesn&#39;t own.&lt;/p&gt;
&lt;h2 id=&quot;owning-only-what-you-created&quot;&gt;Owning only what you created&lt;/h2&gt;
&lt;p&gt;The entire safety model rests on one thing: every event this tool creates carries
an &lt;a rel=&quot;external&quot; href=&quot;https://learn.microsoft.com/en-us/graph/api/resources/opentypeextension&quot;&gt;open extension&lt;/a&gt;
named &lt;code&gt;timetable-calendar-generator&lt;/code&gt;. When it reads a mailbox back, it filters for
exactly that:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;start/dateTime gt &amp;#39;...&amp;#39; and start/dateTime lt &amp;#39;...&amp;#39; and extensions/any(f:f/id eq &amp;#39;timetable-calendar-generator&amp;#39;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing outside that filter — a user&#39;s own meeting, a lunch reservation, an event
from before the sync window — is ever in scope for deletion. The tool literally
cannot see it. This is what makes &quot;reconcile against desired state&quot; safe instead of
terrifying: the blast radius is bounded to the tool&#39;s own tagged events, inside the
sync window, full stop.&lt;/p&gt;
&lt;h2 id=&quot;the-diff-key&quot;&gt;The diff key&lt;/h2&gt;
&lt;p&gt;Reconciliation needs a way to say &quot;is this desired event already present.&quot; A
naive approach — comparing full event objects — makes trivial differences (someone
retitles an event, whitespace changes) look like a delete-and-recreate every night.
Instead, each event reduces to a key:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;|&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;|&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;end&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;|&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;location&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;with one deliberate wrinkle: &lt;strong&gt;the title is truncated at the first &lt;code&gt;-&lt;/code&gt;&lt;/strong&gt;. Arbor
titles like &lt;code&gt;Maths: Y10Ma/1&lt;/code&gt; can pick up manual suffixes after a dash, and those
shouldn&#39;t cause the tool to think the event changed. Two full snapshots — what
Arbor says should exist, what&#39;s currently tagged in the mailbox — get reduced to
sets of these keys, and the diff is just set arithmetic: keys desired but absent
get added, keys present but not desired get deleted, everything else is left alone.&lt;/p&gt;
&lt;p&gt;Duplicate keys are handled the same way in both directions: for desired events, a
&lt;code&gt;Group-Object&lt;/code&gt; on the key keeps only the first (a defensive de-dupe on Arbor&#39;s
side); for existing events, if a key has more than one tagged event, every copy
past the first is queued for deletion. That&#39;s what cleans up duplicates from the
next quirk.&lt;/p&gt;
&lt;h2 id=&quot;categories-and-reminders-come-from-the-title-not-from-arbor&quot;&gt;Categories and reminders come from the title, not from Arbor&lt;/h2&gt;
&lt;p&gt;Arbor doesn&#39;t tell you &quot;this is a duty&quot; versus &quot;this is a lesson&quot; versus &quot;this is a
meeting&quot; in a structured field — that distinction lives in how the event is
titled. So category assignment is keyword matching against the title (&lt;code&gt;duty&lt;/code&gt;,
&lt;code&gt;duties&lt;/code&gt; → Duty; &lt;code&gt;meet&lt;/code&gt;, &lt;code&gt;line management&lt;/code&gt;, &lt;code&gt;brief&lt;/code&gt;, &lt;code&gt;mentor&lt;/code&gt;, &lt;code&gt;lmm&lt;/code&gt;, &lt;code&gt;review&lt;/code&gt;,
&lt;code&gt;planning&lt;/code&gt;, &lt;code&gt;discuss&lt;/code&gt; → Meeting; everything else → Timetable), with a reminder
switched on specifically for duty events. The three master categories
(&lt;code&gt;Timetable&lt;/code&gt;/preset2, &lt;code&gt;Duty&lt;/code&gt;/preset10, &lt;code&gt;Meeting&lt;/code&gt;/preset4) get created on a mailbox
the first time the tool touches it, if they&#39;re not already there.&lt;/p&gt;
&lt;p&gt;This is inherently a bit fragile — it only works as well as the school&#39;s own
timetable naming conventions — but it means zero configuration per event type, and
it matches the categorisation scheme staff were already used to from &lt;code&gt;makecal&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;batching-and-the-503-that-isn-t-a-failure&quot;&gt;Batching and the 503-that-isn&#39;t-a-failure&lt;/h2&gt;
&lt;p&gt;Writes go through Microsoft Graph&#39;s &lt;code&gt;$batch&lt;/code&gt; endpoint, four requests per batch —
that&#39;s the concurrency limit per mailbox, not an arbitrary tuning choice. Batch
requests get the same exponential-backoff retry as reads, and honour a
&lt;code&gt;Retry-After&lt;/code&gt; header when Graph sends one.&lt;/p&gt;
&lt;p&gt;One specific failure mode drove the shape of the whole sync loop: &lt;strong&gt;Graph
sometimes returns a 503 for an event creation that actually succeeded anyway.&lt;/strong&gt;
Retrying a &quot;failed&quot; request that wasn&#39;t really a failure means the event gets
created twice. Rather than trying to make error handling smart enough to detect
this in the moment (there isn&#39;t a reliable signal for it), the fix is structural:
the whole read-diff-write cycle runs as a loop, up to five passes, and only stops
early once a pass finds nothing left to add. A duplicate created by a false 503 on
pass 1 simply gets caught by the diff on pass 2 and deleted. The same idea handles
DELETE calls that 404 — Graph&#39;s eventual consistency can show an event as still
existing on a pass after an earlier delete already removed it, so a 404 on delete
is treated as success, not an error, since the desired end state (event gone) is
already true.&lt;/p&gt;
&lt;h2 id=&quot;idempotency-as-the-actual-test&quot;&gt;Idempotency as the actual test&lt;/h2&gt;
&lt;p&gt;None of the above matters if running the sync twice in a row does anything at all.
That&#39;s the property the whole design optimises for, and it&#39;s also the simplest
possible test: run it, run it again, and the second run should report zero adds
and zero deletes. If it doesn&#39;t, something in the key derivation or the tagging
is wrong — and that&#39;s a much easier bug to reason about than &quot;why did this user&#39;s
calendar end up with a duplicate event.&quot;&lt;/p&gt;
&lt;h2 id=&quot;parallel-but-no-differently&quot;&gt;Parallel, but no differently&lt;/h2&gt;
&lt;p&gt;25 mailboxes sync concurrently (&lt;code&gt;ForEach-Object -Parallel&lt;/code&gt;, each runspace with its
own Graph connection), because doing users one at a time against a tenant with a
few hundred staff would take too long to run nightly. But it&#39;s parallel &lt;em&gt;users&lt;/em&gt;,
not parallel logic — each mailbox goes through exactly the same read-diff-write-
retry cycle described above, independently. Concurrency is a property of the
outer loop, not something the reconciliation logic has to know about.&lt;/p&gt;
&lt;h2 id=&quot;watching-it-work&quot;&gt;Watching it work&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;-ShowChanges&lt;/code&gt; prints only the add/delete lines per mailbox — what actually moved
— and &lt;code&gt;-ShowEvents&lt;/code&gt; prints everything, including the events left untouched, which
is the fastest way to convince yourself (or a colleague) that the tool really is
leaving unrelated events alone. Both were built for exactly the kind of trust-but-
verify checking this series has been about: not &quot;does the code look right,&quot; but
&quot;can I watch it, on a real mailbox, and confirm it only ever touches what it&#39;s
supposed to.&quot;&lt;/p&gt;
&lt;p&gt;That&#39;s the three-part shape of this project: an idea borrowed from prior art
(post 1), a data source that took real digging to use correctly (post 2), and a
reconciliation engine whose entire design is in service of one property —
safe to run unattended, forever.&lt;/p&gt;
&lt;!-- TODO: replace with repo name/URL before publishing --&gt;
&lt;p&gt;The full source — &lt;code&gt;Get-ArborTimetable.ps1&lt;/code&gt;, &lt;code&gt;Sync-OutlookCalendars.ps1&lt;/code&gt;, and the
&lt;code&gt;TimetableSync&lt;/code&gt; module — is up on GitHub at &lt;strong&gt;&lt;a href=&quot;/blog/arbor-calendar-sync-reconciliation-engine/REPO_URL&quot;&gt;REPO_NAME&lt;/a&gt;&lt;/strong&gt;, MIT
licensed. Issues and PRs welcome, especially from anyone hitting Arbor quirks
we haven&#39;t.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Your timetable is in the wrong app</title>
        <published>2026-07-23T00:00:00+00:00</published>
        <updated>2026-07-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            Paul Richards
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="/blog/arbor-calendar-sync-the-idea/"/>
        <id>/blog/arbor-calendar-sync-the-idea/</id>
        
        <content type="html" xml:base="/blog/arbor-calendar-sync-the-idea/">&lt;h1 id=&quot;your-timetable-is-in-the-wrong-app&quot;&gt;Your timetable is in the wrong app&lt;/h1&gt;
&lt;p&gt;Every school running a MIS has the same quiet problem: the timetable — the thing
that tells a member of staff where to be, when, and with which class — lives in a
system that people only open to do MIS things. Register a class. Log a behaviour
point. Check a report. It&#39;s not the tool anyone has open at 8:55am when they&#39;re
trying to remember if period 3 moved rooms because of the mocks.&lt;/p&gt;
&lt;p&gt;The tool people actually have open is their calendar. So the idea is simple to
state: every lesson, duty, and meeting a staff member has should show up as an
event in their Outlook calendar, automatically, kept in sync with the MIS,
without anyone re-typing anything.&lt;/p&gt;
&lt;p&gt;Simple to state. Less simple to do safely — because a calendar sync that gets it
wrong doesn&#39;t fail quietly. It double-books someone, deletes an event they added
themselves, or leaves stale entries for a lesson that got moved. Whatever we
built had to be something we&#39;d trust to run unattended, every night, indefinitely.&lt;/p&gt;
&lt;h2 id=&quot;we-didn-t-start-from-a-blank-page&quot;&gt;We didn&#39;t start from a blank page&lt;/h2&gt;
&lt;p&gt;This idea isn&#39;t ours. &lt;a rel=&quot;external&quot; href=&quot;https://github.com/jamesgurung/timetable-calendar-generator&quot;&gt;&lt;code&gt;timetable-calendar-generator&lt;/code&gt;&lt;/a&gt;
by James Gurung has been doing exactly this for schools running SIMS for a while,
and it&#39;s the reason we didn&#39;t have to work out the hard part from scratch. The
piece of design worth stealing wasn&#39;t &quot;call an API and make some events&quot; — it&#39;s
&lt;em&gt;how&lt;/em&gt; it stays safe to run on a schedule forever:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Desired-state, not deltas.&lt;/strong&gt; Each run computes the full set of events that
&lt;em&gt;should&lt;/em&gt; exist for a user in the sync window, rather than trying to track what
changed since last time. That sidesteps an entire category of bugs where a
missed run or a crash leaves things half-applied.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only touch what you own.&lt;/strong&gt; Every event the tool creates is tagged. On the
next run, it reconciles &lt;em&gt;only&lt;/em&gt; the tagged events against the desired state —
add what&#39;s missing, remove what&#39;s tagged but no longer listed, leave everything
else (a user&#39;s own meetings, anything outside the window) completely alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Idempotent by construction.&lt;/strong&gt; Because of the two points above, running the
sync twice in a row is a no-op. That&#39;s what makes &quot;just run it every night&quot; a
reasonable operational model instead of a leap of faith.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&#39;s the actual idea this series is about. Everything else — which MIS, which
language, which API — is implementation detail.&lt;/p&gt;
&lt;h2 id=&quot;except-the-details-didn-t-line-up&quot;&gt;Except the details didn&#39;t line up&lt;/h2&gt;
&lt;p&gt;We&#39;re not a SIMS school; we&#39;re an Arbor one. Arbor&#39;s API is a GraphQL endpoint,
not SIMS&#39;, with its own set of quirks we had to learn the hard way — that&#39;s
post 2. So the sync logic needed rebuilding against a different data source
regardless.&lt;/p&gt;
&lt;p&gt;Given that, we also changed the implementation language: PowerShell instead of
C#. Not because C# did anything wrong, but because of who has to maintain this
in five years. A school IT team already lives in PowerShell — it&#39;s what
provisions AD accounts, runs scheduled maintenance, talks to Microsoft Graph.
There&#39;s no build step, no project to open, no framework version to keep current.
Anyone on the team can open the script, read it top to bottom, and change a line.
For a small tool that has to outlive whoever wrote it, that maintainability
matters more than any performance or ergonomics C# would have offered.&lt;/p&gt;
&lt;h2 id=&quot;what-we-built&quot;&gt;What we built&lt;/h2&gt;
&lt;p&gt;Two stages, matching the model above:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pull&lt;/strong&gt; the desired state from Arbor — every current staff member&#39;s lessons,
duties, and meetings for the sync window — into a plain JSON file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reconcile&lt;/strong&gt; each Outlook calendar against that file: tagged events not in
the file get deleted, missing ones get added, everything else is untouched.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Running stage 2 twice back to back changes nothing. Running it every night,
indefinitely, is exactly the plan.&lt;/p&gt;
&lt;p&gt;The next two posts go into how each stage actually works: making sense of
Arbor&#39;s GraphQL API and its rougher edges — rate limits, fields that only ever
return the current academic year, staff filters that don&#39;t mean what they say —
and then the reconciliation engine itself: the diffing, the tagging, the parallel
writes to Microsoft Graph, and the edge cases that only show up once real staff
data hits it.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Building My New Website with Zola — Keeping It Simple</title>
        <published>2026-07-23T00:00:00+00:00</published>
        <updated>2026-07-23T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/blog/website/"/>
        <id>/blog/website/</id>
        
        <content type="html" xml:base="/blog/website/">&lt;p&gt;For small to medium sized web development, people are increasingly reaching for Squarespace-type builders, or WordPress with an Elementor-type visual builder. These solutions aren&#39;t necessarily the most suitable, for a few key reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You don&#39;t own your data&lt;/strong&gt; with some of the online builders — so you&#39;re at their mercy when it comes to pricing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WordPress can easily be abused&lt;/strong&gt; with bloated visual builders and too many plugins. This creates performance problems, opens up security issues, and makes keeping the site up to date a major feat. Within the education space, outdated WordPress instances and plugins have been highlighted as a particular concern in 2024.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A small to medium sized digital presence needs to offer the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Data Ownership&lt;/strong&gt; — You should be able to both shape your data and own it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Durability&lt;/strong&gt; — A website should be long-lasting, stable and easy to maintain. It shouldn&#39;t break constantly because a handful of third-party plugins are conflicting or need constant updates. It should just work, and be kept up to date with simple, occasional maintenance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ease of Use&lt;/strong&gt; — A competent developer or designer should be able to set up the design, hosting, and maintenance. The end user should be able to focus purely on the content: add a new blog post weekly, update a staff member&#39;s title, swap out a photo, add an event, and so on.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;a-bit-of-history&quot;&gt;A bit of history&lt;/h2&gt;
&lt;p&gt;Since the late 1990s I&#39;ve had the opportunity to develop a number of websites and intranets within the education space where I&#39;ve worked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;School intranet — 1997/1998 (not sure I&#39;ve still got any screenshots of this one)&lt;/li&gt;
&lt;li&gt;University project — 2001 — &lt;a href=&quot;/blog/website/uni-report.pdf&quot;&gt;University Project Report&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;School website — Microsoft FrontPage — 2000–2003&lt;/li&gt;
&lt;li&gt;School website — PHP — 2004/2005&lt;/li&gt;
&lt;li&gt;School intranet — 2001–2013&lt;/li&gt;
&lt;li&gt;School website — ASP.NET — ~2006–2011&lt;/li&gt;
&lt;li&gt;School intranet — ASP.NET, &lt;a rel=&quot;external&quot; href=&quot;https://github.com/techienickb/hap&quot;&gt;Home Access Plus&lt;/a&gt; — patches contributed 2013–2014, in use until 2022&lt;/li&gt;
&lt;li&gt;School website — PHP, &lt;a rel=&quot;external&quot; href=&quot;https://getgrav.org&quot;&gt;Grav&lt;/a&gt; — 2015–2021&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Between 2000 and 2013 the focus was always on building something with an integrated database. By 2015 I&#39;d started thinking about static site generators — Grav was (and still is) great to use, but it was never quite what I wanted.&lt;/p&gt;
&lt;p&gt;None of these were built on WordPress, so this isn&#39;t a &quot;migration&quot; story — it&#39;s more that WordPress and its site-builder relatives are the default a lot of people reach for, and I wanted to set out why I&#39;ve gone a different way for this new site.&lt;/p&gt;
&lt;h2 id=&quot;keeping-it-simple&quot;&gt;Keeping it simple&lt;/h2&gt;
&lt;p&gt;It&#39;s a great time to be a web developer, but I&#39;ve been looking for a way to &quot;keep it simple.&quot; This &lt;a rel=&quot;external&quot; href=&quot;https://shikhashikz.com/blog/an-upgrade-to-latest-web-technology/&quot;&gt;blog post on moving from WordPress to Astro&lt;/a&gt; makes a similar case for a static build. For this new site, after weighing up a few options, I&#39;ve settled on &lt;strong&gt;Zola&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I initially looked A CMS Layer( see &lt;a href=&quot;/blog/too-many-options-cms/&quot;&gt;&quot;Too many options&quot;&lt;/a&gt; ) then looked at Hugo with Hugo Blox, but landed on Zola instead: it&#39;s a single self-contained binary (no Node/npm toolchain to babysit), has Sass compilation and asset processing built in, and its templating (Tera) is straightforward to pick up. Content lives as plain Markdown files in the repo — no CMS layer, no database, no admin panel to secure or update. Just files, git, and a build step.&lt;/p&gt;
&lt;h2 id=&quot;the-plan&quot;&gt;The plan&lt;/h2&gt;
&lt;p&gt;The aim of this series of posts is to document building this new site from scratch, and to use the experience as a research exercise for evaluating Zola as a solid setup for other projects too — including, potentially, sites at the school trust I work for.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set up the new site with Zola, managing all content as Markdown.&lt;/li&gt;
&lt;li&gt;Build out a theme using Tailwind CSS.&lt;/li&gt;
&lt;li&gt;Publish the site statically via Cloudflare.&lt;/li&gt;
&lt;li&gt;Use the build as a proof of concept for:
&lt;ol&gt;
&lt;li&gt;Defining a clean content structure/schema for a small site.&lt;/li&gt;
&lt;li&gt;Keeping the whole workflow git-based, with no CMS or database dependency.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Prove out ease of use and speed of development within a modern stack — can the above be achieved within 4–5 days?&lt;/li&gt;
&lt;/ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Useful Links</title>
        <published>2026-07-23T00:00:00+00:00</published>
        <updated>2026-07-23T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/useful-links/"/>
        <id>/pages/useful-links/</id>
        
        <content type="html" xml:base="/pages/useful-links/"></content>
        
    </entry>
    <entry xml:lang="en">
        <title>About</title>
        <published>2026-07-22T00:00:00+00:00</published>
        <updated>2026-07-22T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/about/"/>
        <id>/pages/about/</id>
        
        <content type="html" xml:base="/pages/about/">&lt;h2 id=&quot;who-am-i&quot;&gt;Who am I?&lt;/h2&gt;
&lt;p&gt;My name is &lt;strong&gt;Paul Richards&lt;/strong&gt;, based in London.
I&#39;m a Network Manager with over 20 years&#39; experience managing school server, network, and client infrastructure, MIS/timetabling systems, and IT support teams — currently at North Star Community Trust.&lt;/p&gt;
&lt;h2 id=&quot;what-i-do&quot;&gt;What I do&lt;/h2&gt;
&lt;p&gt;I work with the &lt;strong&gt;Microsoft ecosystem&lt;/strong&gt; — Power Platform (Power Apps, Power Automate, Copilot Studio), Microsoft 365, and IT infrastructure automation.
Over the years I&#39;ve built things like help desk systems, school timetable/calendar sync tools, and security hardening checks, alongside smaller scripts and utilities.&lt;/p&gt;
&lt;h2 id=&quot;what-s-on-this-site&quot;&gt;What&#39;s on this site&lt;/h2&gt;
&lt;p&gt;This is my personal corner of the web, covering three things I care about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Photos&lt;/strong&gt; — travel shots from places I&#39;ve visited&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code&lt;/strong&gt; — notes and projects from my development work, mostly around Microsoft/Power Platform tooling and general programming&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Microsoft tips&lt;/strong&gt; — practical tricks and lessons learned from working day-to-day with Microsoft 365, Power Platform, and Windows&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;get-in-touch&quot;&gt;Get in touch&lt;/h2&gt;
&lt;p&gt;You can find me on &lt;a rel=&quot;external&quot; href=&quot;https://github.com/Mantis&quot;&gt;GitHub&lt;/a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https://www.linkedin.com/in/paul-richards-076801b4/&quot;&gt;LinkedIn&lt;/a&gt;, or via the &lt;a href=&quot;/pages/contact/&quot;&gt;contact page&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Contact</title>
        <published>2026-07-22T00:00:00+00:00</published>
        <updated>2026-07-22T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/contact/"/>
        <id>/pages/contact/</id>
        
        <content type="html" xml:base="/pages/contact/">&lt;h2 id=&quot;get-in-touch&quot;&gt;Get in touch&lt;/h2&gt;
&lt;p&gt;Feel free to reach out via email at &lt;a href=&quot;mailto:paul@minimoo.org&quot;&gt;paul@minimoo.org&lt;/a&gt;, or via &lt;a rel=&quot;external&quot; href=&quot;https://github.com/Mantis&quot;&gt;GitHub&lt;/a&gt; or &lt;a rel=&quot;external&quot; href=&quot;https://www.linkedin.com/in/paul-richards-076801b4/&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Resume / Portfolio</title>
        <published>2026-07-22T00:00:00+00:00</published>
        <updated>2026-07-22T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/resume/"/>
        <id>/pages/resume/</id>
        
        <content type="html" xml:base="/pages/resume/"></content>
        
    </entry>
    <entry xml:lang="en">
        <title>Too Many Options: Picking a CMS and Learning When to Stop Looking</title>
        <published>2026-07-20T00:00:00+00:00</published>
        <updated>2026-07-20T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/blog/too-many-options-cms/"/>
        <id>/blog/too-many-options-cms/</id>
        
        <content type="html" xml:base="/blog/too-many-options-cms/">&lt;p&gt;Most of my CMS experience comes from an older world: PHP and ASP.NET systems with a database sitting behind an admin panel. For this round, I wanted to look forward rather than back — modern static site generators, paired with a CMS built around JavaScript/Node or Go, rather than reaching for what I already knew.&lt;/p&gt;
&lt;p&gt;What followed was a genuinely useful evaluation. It was also, if I&#39;m honest, a bit of a rabbit hole.&lt;/p&gt;
&lt;h2 id=&quot;what-i-looked-at&quot;&gt;What I looked at&lt;/h2&gt;
&lt;h3 id=&quot;keystatic&quot;&gt;Keystatic&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https://keystatic.com/&quot;&gt;Keystatic&lt;/a&gt; bills itself as a tool that makes Markdown, JSON and YAML content in your codebase editable by humans.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/too-many-options-cms/keystatic.png&quot; alt=&quot;keystatic&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A few things stood out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can define both &lt;strong&gt;collections&lt;/strong&gt; (repeating content, like blog posts) and &lt;strong&gt;singletons&lt;/strong&gt; (one-off pages, like a homepage or settings file).&lt;/li&gt;
&lt;li&gt;With a bit of cleverness in the config, you can approximate a block-builder-style editing experience for more flexible page layouts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/blog/too-many-options-cms/keystatic-2.png&quot; alt=&quot;keystatic 2&quot; /&gt;
&lt;img src=&quot;/blog/too-many-options-cms/keystatic-3.png&quot; alt=&quot;keystatic 3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The catch, in testing against a larger dataset: with around 2,500 blog posts in a collection, Keystatic was pulling roughly 15MB from the server just to populate the admin UI. Workable for a small site, but worth knowing about before committing to it for anything with real scale.&lt;/p&gt;
&lt;h3 id=&quot;decap-cms&quot;&gt;Decap CMS&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https://decapcms.org/&quot;&gt;Decap CMS&lt;/a&gt; — the project formerly known as Netlify CMS — pitches itself as usable with any static site generator, for a faster and more flexible web project.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/too-many-options-cms/decap-1.png&quot; alt=&quot;decap 1&quot; /&gt;
&lt;img src=&quot;/blog/too-many-options-cms/decap-2.png&quot; alt=&quot;decap 2&quot; /&gt;
&lt;img src=&quot;/blog/too-many-options-cms/decap-3.png&quot; alt=&quot;decap 3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It&#39;s the most established option here: git-based, generator-agnostic, and with years of production use behind it. That maturity is reassuring, though the project&#39;s development pace has visibly slowed compared to some of the newer alternatives.&lt;/p&gt;
&lt;h3 id=&quot;sveltia-cms&quot;&gt;Sveltia CMS&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/blog/too-many-options-cms/sveltia-1.png&quot; alt=&quot;sveltia 1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Sveltia CMS is a newer, from-scratch reimplementation aimed squarely at Decap&#39;s config format — so it&#39;s largely a drop-in replacement — but with a modern rebuild under the hood and a noticeably snappier editing experience.&lt;/p&gt;
&lt;h3 id=&quot;gatsby-decap-cms&quot;&gt;Gatsby + Decap CMS&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/blog/too-many-options-cms/gatsby-1.png&quot; alt=&quot;gatsby 1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Rounding things out, I also looked at a Gatsby front end paired with Decap CMS — a combination that was extremely common a few years ago and is still floating around in plenty of tutorials and starter templates.&lt;/p&gt;
&lt;h2 id=&quot;the-actual-problem&quot;&gt;The actual problem&lt;/h2&gt;
&lt;p&gt;None of the above is a bad option. That&#39;s precisely what made this hard.&lt;/p&gt;
&lt;p&gt;Four viable tools, each with a slightly different trade-off — data footprint, maturity, speed, ecosystem — and each perfectly capable of doing the job. Add in the generators they can pair with, the themes, the hosting choices, and the total number of &quot;reasonable&quot; combinations balloons fast. At some point the research stops being research and starts being a way of avoiding a decision.&lt;/p&gt;
&lt;p&gt;Barry Schwartz wrote about this in &lt;em&gt;The Paradox of Choice&lt;/em&gt;: more options don&#39;t reliably make us happier with our eventual choice — past a certain point, they make us less confident in it, because there&#39;s always another combination we haven&#39;t checked yet. Software tooling has the same trap, just dressed up as diligence. Reading one more comparison post feels like progress. Often it&#39;s just a well-disguised form of procrastination.&lt;/p&gt;
&lt;p&gt;There&#39;s a useful distinction from that same research between &lt;strong&gt;maximisers&lt;/strong&gt;, who try to evaluate every option to find the objectively best one, and &lt;strong&gt;satisficers&lt;/strong&gt;, who define what &quot;good enough&quot; looks like and stop once something clears that bar. Maximisers tend to spend far more time deciding and end up less satisfied with the result, because they can never fully rule out that something better exists. For a solo project with a deadline, satisficing isn&#39;t a compromise — it&#39;s the correct strategy.&lt;/p&gt;
&lt;h2 id=&quot;breaking-the-loop&quot;&gt;Breaking the loop&lt;/h2&gt;
&lt;p&gt;A few things that actually helped move this from &quot;still evaluating&quot; to &quot;decided&quot;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Write down the must-haves first, before the options.&lt;/strong&gt; Markdown/JSON/YAML source of truth, git-based workflow, reasonable editing experience, no database to maintain. Anything meeting all of those is a candidate; everything else is noise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set a decision deadline.&lt;/strong&gt; Not &quot;when I&#39;m sure,&quot; but a date. Certainty rarely arrives — you just run out of time to keep second-guessing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accept that &quot;good enough, shipped&quot; beats &quot;perfect, still in a spreadsheet.&quot;&lt;/strong&gt; A CMS that&#39;s live and being used teaches you far more than another week of feature comparisons.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write down &lt;em&gt;why&lt;/em&gt;, not just &lt;em&gt;what&lt;/em&gt;.&lt;/strong&gt; The reasoning is what lets you revisit the decision sensibly later, rather than relitigating it from scratch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat the choice as reversible.&lt;/strong&gt; Content in Markdown, in git, isn&#39;t locked into any one tool. Picking &quot;wrong&quot; mostly costs a migration, not a rebuild.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;where-i-landed&quot;&gt;Where I landed&lt;/h2&gt;
&lt;p&gt;For this site specifically, the honest answer turned out to be: no CMS at all. Content lives as plain Markdown files, edited directly, with Zola handling the build. It&#39;s the smallest possible surface area, and it satisfies every one of the must-haves above without adding a dependency.&lt;/p&gt;
&lt;p&gt;That&#39;s not a verdict on Keystatic, Decap or Sveltia — all three remain solid choices, and I&#39;d reach for Decap or Sveltia without hesitation on a project where non-technical editors need a proper admin UI. It&#39;s simply a reminder that the best tool is the one that matches the actual job in front of you, not the one that wins the most comparison points on paper. Sometimes the most productive outcome of an evaluation is realising you didn&#39;t need the extra layer in the first place.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Adding audio to your blog</title>
        <published>2026-02-11T00:00:00+00:00</published>
        <updated>2026-02-11T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/cms/audio-player-shortcode/"/>
        <id>/pages/cms/audio-player-shortcode/</id>
        
        <content type="html" xml:base="/pages/cms/audio-player-shortcode/">&lt;p&gt;There are two practical ways to add audio to a blog: use the native &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element or render a custom player with JavaScript.
The &lt;a rel=&quot;external&quot; href=&quot;https://www.getzola.org/&quot;&gt;Zola&lt;/a&gt; theme &lt;a rel=&quot;external&quot; href=&quot;https://github.com/thomasweitzel/zolarwind&quot;&gt;Zolarwind&lt;/a&gt; provides two shortcodes that map to those choices: &lt;code&gt;audio_simple&lt;/code&gt; uses the native element, &lt;code&gt;audio&lt;/code&gt; uses a custom player.
Both are usable as-is, and the choice comes down to how much control you want over the player UI.&lt;/p&gt;
&lt;h2 id=&quot;the-baseline-native-audio&quot;&gt;The baseline native &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;You get built-in controls and good accessibility.
If you are fine with browser-native styling, this is the fastest path.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-d-2&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-1&quot;&gt;audio&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-18&quot;&gt; controls&lt;/span&gt;&lt;span class=&quot;z-d-2&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-d-2&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-1&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-18&quot;&gt; src&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;audio.mp3&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-18&quot;&gt; type&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;audio/mpeg&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-d-2&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-d-2&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;z-l-3 z-d-1&quot;&gt;audio&lt;/span&gt;&lt;span class=&quot;z-d-2&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;audio controls&gt;
  &lt;source src=&quot;audio.mp3&quot; type=&quot;audio/mpeg&quot;&gt;
&lt;/audio&gt;
&lt;p&gt;The tradeoff is consistency. Browsers style the element differently, and you get limited control over the UI.&lt;/p&gt;
&lt;h2 id=&quot;audio-simple-shortcode&quot;&gt;&lt;code&gt;audio_simple&lt;/code&gt; shortcode&lt;/h2&gt;
&lt;p&gt;This wraps the native element and keeps the setup small.
It uses no JavaScript, like the baseline audio element, but adds a small amount of CSS for layout.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;container max-w-md my-6 py-6 px-6 text-black dark:text-black bg-gradient-to-tr from-ok-600 to-primary-800 rounded-2xl overflow-hidden&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;text-center relative&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;svg &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;xmlns&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;http://www.w3.org/2000/svg&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; fill&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; viewBox&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;0 0 24 24&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; stroke-width&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;1.5&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; stroke&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;currentColor&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;h-16 w-16 inline-block&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            &amp;lt;path &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;stroke-linecap&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; stroke-linejoin&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; d&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;/svg&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;mb-6 text-center relative&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;h2 &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;inline-block font-medium&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;Audio in your blog&amp;lt;/h2&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;p &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;inline-block font-medium text-sm&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;Female Speaker | Blog of Thomas Weitzel | 2026&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;audio controls &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;audio.mp3&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; style&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;width: 100%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;Your browser does not support the audio element.&amp;lt;/audio&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&quot;container max-w-md my-6 py-6 px-6 text-black dark:text-black bg-gradient-to-tr from-ok-600 to-primary-800 rounded-2xl overflow-hidden&quot;&gt;
    &lt;div class=&quot;text-center relative&quot;&gt;
        &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke-width=&quot;1.5&quot; stroke=&quot;currentColor&quot; class=&quot;h-16 w-16 inline-block&quot;&gt;
            &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; d=&quot;M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z&quot; /&gt;
        &lt;/svg&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mb-6 text-center relative&quot;&gt;
        &lt;h2 class=&quot;inline-block font-medium&quot;&gt;Audio in your blog&lt;/h2&gt;
        &lt;p class=&quot;inline-block font-medium text-sm&quot;&gt;Female Speaker | Blog of Thomas Weitzel | 2026&lt;/p&gt;
    &lt;/div&gt;
    &lt;audio controls src=&quot;audio.mp3&quot; style=&quot;width: 100%&quot;&gt;Your browser does not support the audio element.&lt;/audio&gt;
&lt;/div&gt;
&lt;h2 id=&quot;custom-player&quot;&gt;Custom player&lt;/h2&gt;
&lt;p&gt;If you want a consistent player UI across browsers, the &lt;code&gt;audio&lt;/code&gt; shortcode renders a custom player with JavaScript.
The native &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; controls are not used; the UI is fully custom.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;div&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;container max-w-md my-6 py-6 px-6 text-black dark:text-black bg-gradient-to-tr from-primary-600 to-warn-800 rounded-2xl overflow-hidden&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  data-audio-id&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  data-audio-src&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;audio.mp3&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;text-center relative&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;svg &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;xmlns&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;http://www.w3.org/2000/svg&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; fill&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; viewBox&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;0 0 24 24&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; stroke-width&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;1.5&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; stroke&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;currentColor&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;h-16 w-16 inline-block&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            &amp;lt;path &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;stroke-linecap&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; stroke-linejoin&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; d&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;/svg&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;mb-6 text-center relative&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;h2 &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;inline-block font-medium&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;Audio in your blog&amp;lt;/h2&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;p &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;inline-block font-medium text-sm&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;Female Speaker | Blog of Thomas Weitzel | 2026&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;audio-controls1 flex space-x-1&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;play-div1 flex-none hover:text-fail-600&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;Loading ...&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;time-div1 flex-none pr-2&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;00:00 | 0:48&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;flex-auto cursor-pointer select-none mt-2&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            &amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;progress-container1 w-full bg-primary-700 rounded-full h-2 dark:bg-neutral-200&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                &amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;progress-bar1 bg-neutral-800 h-2 rounded-full hover:bg-fail-600&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; style&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;width: 0%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;div &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;speaker-div1 flex-none pl-2 hover:text-fail-600&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;/div&amp;gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;/js/audio/howler.js&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; integrity&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;sha384-B3AhJtVepaXjzRprLH6ZMkrOI+XDduqXmwhe4m+RDATUkM8o1I2STFsig6dra4XF&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;script &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;/js/audio/audio-player.js&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; integrity&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;sha384-uM+RjXS/SiYym8NXLgoHFdNXVU0dZFjg9FyWhppQgkb0RJd1dnp7H+Pom5azyaMI&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;script &lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;/js/audio/audio-init.js&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; integrity&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;sha384-MxFc2CYeIiCM4XDzrORmIy9R06BD/FQLTHq71ZIlTY/tJCqi3Isa8/o+yKSFg1Vc&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/script&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div
  class=&quot;container max-w-md my-6 py-6 px-6 text-black dark:text-black bg-gradient-to-tr from-primary-600 to-warn-800 rounded-2xl overflow-hidden&quot;
  data-audio-id=&quot;1&quot;
  data-audio-src=&quot;audio.mp3&quot;
&gt;
    &lt;div class=&quot;text-center relative&quot;&gt;
        &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke-width=&quot;1.5&quot; stroke=&quot;currentColor&quot; class=&quot;h-16 w-16 inline-block&quot;&gt;
            &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; d=&quot;M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z&quot; /&gt;
        &lt;/svg&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mb-6 text-center relative&quot;&gt;
        &lt;h2 class=&quot;inline-block font-medium&quot;&gt;Audio in your blog&lt;/h2&gt;
        &lt;p class=&quot;inline-block font-medium text-sm&quot;&gt;Female Speaker | Blog of Thomas Weitzel | 2026&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&quot;audio-controls1 flex space-x-1&quot;&gt;
        &lt;div class=&quot;play-div1 flex-none hover:text-fail-600&quot;&gt;Loading ...&lt;/div&gt;
        &lt;div class=&quot;time-div1 flex-none pr-2&quot;&gt;00:00 | 0:48&lt;/div&gt;
        &lt;div class=&quot;flex-auto cursor-pointer select-none mt-2&quot;&gt;
            &lt;div class=&quot;progress-container1 w-full bg-primary-700 rounded-full h-2 dark:bg-neutral-200&quot;&gt;
                &lt;div class=&quot;progress-bar1 bg-neutral-800 h-2 rounded-full hover:bg-fail-600&quot; style=&quot;width: 0%&quot;&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;speaker-div1 flex-none pl-2 hover:text-fail-600&quot;&gt;&lt;/div&gt;
    &lt;/div&gt;&lt;script src=&quot;/js/audio/howler.js&quot; integrity=&quot;sha384-B3AhJtVepaXjzRprLH6ZMkrOI+XDduqXmwhe4m+RDATUkM8o1I2STFsig6dra4XF&quot;&gt;&lt;/script&gt;
        &lt;script src=&quot;/js/audio/audio-player.js&quot; integrity=&quot;sha384-uM+RjXS/SiYym8NXLgoHFdNXVU0dZFjg9FyWhppQgkb0RJd1dnp7H+Pom5azyaMI&quot;&gt;&lt;/script&gt;
        &lt;script src=&quot;/js/audio/audio-init.js&quot; integrity=&quot;sha384-MxFc2CYeIiCM4XDzrORmIy9R06BD/FQLTHq71ZIlTY/tJCqi3Isa8/o+yKSFg1Vc&quot;&gt;&lt;/script&gt;&lt;/div&gt;
&lt;p&gt;I prefer the &lt;code&gt;audio&lt;/code&gt; shortcode, because I want a stable player UI across browsers.
&lt;code&gt;audio_simple&lt;/code&gt; stays in the toolbox because it keeps the setup small and uses no extra JavaScript.&lt;/p&gt;
&lt;h2 id=&quot;notes&quot;&gt;Notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The shortcodes expect local assets, not remote URLs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zola serve&lt;/code&gt; does not support range requests as of 0.22.1, so Chrome cannot seek properly. Use a static server for testing.&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Search</title>
        <published>2026-01-14T00:00:00+00:00</published>
        <updated>2026-01-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            Paul Richards
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/search/"/>
        <id>/pages/search/</id>
        
        <content type="html" xml:base="/pages/search/"></content>
        
    </entry>
    <entry xml:lang="en">
        <title>Diagrams with Mermaid</title>
        <published>2023-10-29T00:00:00+00:00</published>
        <updated>2023-10-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            Thomas Weitzel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/cms/mermaid/"/>
        <id>/pages/cms/mermaid/</id>
        
        <content type="html" xml:base="/pages/cms/mermaid/">&lt;h2 id=&quot;what-are-mermaid-diagrams&quot;&gt;What are Mermaid diagrams?&lt;/h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https://mermaid.js.org&quot;&gt;Mermaid&lt;/a&gt; is a syntax similar to Markdown where you can use text to describe and automatically generate diagrams.
With Mermaid, you can generate
&lt;a rel=&quot;external&quot; href=&quot;https://mermaid.js.org/syntax/flowchart.html&quot;&gt;Flow charts&lt;/a&gt;,
&lt;a rel=&quot;external&quot; href=&quot;https://mermaid.js.org/syntax/classDiagram.html&quot;&gt;UML diagrams&lt;/a&gt;,
&lt;a rel=&quot;external&quot; href=&quot;https://mermaid.js.org/syntax/pie.html&quot;&gt;Pie charts&lt;/a&gt;,
&lt;a rel=&quot;external&quot; href=&quot;https://mermaid.js.org/syntax/gantt.html&quot;&gt;Gantt diagrams&lt;/a&gt;,
&lt;a rel=&quot;external&quot; href=&quot;https://mermaid.js.org/syntax/entityRelationshipDiagram.html&quot;&gt;Entity Relationship diagrams&lt;/a&gt;,
and more.&lt;/p&gt;
&lt;h2 id=&quot;an-entity-relationship-diagram-example&quot;&gt;An Entity Relationship diagram example&lt;/h2&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
  
    %%{ init: {&#39;theme&#39;: &#39;forest&#39;} }%%
  
  
erDiagram
    CUSTOMER ||--o{ ORDER : places
    ORDER ||--|{ LINEITEM : contains
    PRODUCT ||--o{ LINEITEM : is_listed_in
    CUSTOMER {
        string Name
        string Email
        string Address
    }
    PRODUCT {
        string ProductName
        float Price
    }
    ORDER {
        date DateOrdered
        string Status
    }
    LINEITEM {
        int Quantity
    }

&lt;/pre&gt;
&lt;h2 id=&quot;using-mermaid-in-your-blog-post&quot;&gt;Using Mermaid in your blog post&lt;/h2&gt;
&lt;p&gt;The diagram shortcode allows you to easily embed Mermaid diagrams in your blog posts, with an option to configure its appearance.&lt;/p&gt;
&lt;h3 id=&quot;basic-usage&quot;&gt;Basic Usage&lt;/h3&gt;
&lt;p&gt;To use the shortcode in your blog post, you would use the following format:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span&gt;diagram&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; %&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Your&lt;/span&gt;&lt;span&gt; Mermaid&lt;/span&gt;&lt;span&gt; diagram&lt;/span&gt;&lt;span&gt; code&lt;/span&gt;&lt;span&gt; here&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;/&lt;/span&gt;&lt;span&gt;diagram&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; %&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace &lt;em&gt;Your Mermaid diagram code here&lt;/em&gt; with your actual Mermaid diagram code.&lt;/p&gt;
&lt;p&gt;To load the necessary JavaScript to render the Mermaid diagram, you need to enable it in the front matter of your blog post by setting the value for &lt;code&gt;extra.diagram&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+++&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;extra&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;diagram&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt; =&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+++&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;configuring-the-appearance&quot;&gt;Configuring the Appearance&lt;/h3&gt;
&lt;p&gt;The shortcode supports an optional &lt;code&gt;init&lt;/code&gt; parameter that allows you to specify configuration options for Mermaid,
particularly changing the appearance of the diagrams through the theme.&lt;/p&gt;
&lt;p&gt;For example, to apply the &lt;code&gt;forest&lt;/code&gt; theme:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span&gt;diagram&lt;/span&gt;&lt;span&gt; init&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;{&amp;#39;theme&amp;#39;: &amp;#39;forest&amp;#39;}&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; %&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Your&lt;/span&gt;&lt;span&gt; Mermaid&lt;/span&gt;&lt;span&gt; diagram&lt;/span&gt;&lt;span&gt; code&lt;/span&gt;&lt;span&gt; here&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;/&lt;/span&gt;&lt;span&gt;diagram&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; %&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;providing-init-configuration&quot;&gt;Providing init Configuration&lt;/h3&gt;
&lt;p&gt;When using the &lt;code&gt;init&lt;/code&gt; parameter, the configuration should be a string wrapped in double quotes.
Inside this string, use single quotes for keys and values.&lt;/p&gt;
&lt;p&gt;Here&#39;s a more advanced example with multiple configuration options:&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span&gt;diagram&lt;/span&gt;&lt;span&gt; init&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;{&amp;#39;theme&amp;#39;: &amp;#39;forest&amp;#39;, &amp;#39;themeVariables&amp;#39;: {&amp;#39;primaryColor&amp;#39;: &amp;#39;#FF0000&amp;#39;}}&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; %&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Your&lt;/span&gt;&lt;span&gt; Mermaid&lt;/span&gt;&lt;span&gt; diagram&lt;/span&gt;&lt;span&gt; code&lt;/span&gt;&lt;span&gt; here&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;/&lt;/span&gt;&lt;span&gt;diagram&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; %&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, we&#39;re using the &lt;code&gt;forest&lt;/code&gt; theme and changing the primary color to red (&lt;code&gt;#FF0000&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Ensure that you use single quotes inside the double quotes for the configuration to work correctly.&lt;/p&gt;
&lt;h3 id=&quot;putting-it-all-together&quot;&gt;Putting it all together&lt;/h3&gt;
&lt;p&gt;Use the shortcode by enclosing your Mermaid diagram code with it.
To change the look of the Mermaid theme, provide values for the &lt;code&gt;init&lt;/code&gt; parameter.
Always enclose the &lt;code&gt;init&lt;/code&gt; value with double quotes, and use single quotes inside the configuration string.
With this shortcode in place, integrating and customizing Mermaid diagrams in your blog posts becomes easy.&lt;/p&gt;
&lt;h2 id=&quot;explaining-mermaid-diagram-code&quot;&gt;Explaining Mermaid diagram code&lt;/h2&gt;
&lt;p&gt;Here is how the diagram rendered above is embedded in this blog post as a code block.
Because it is the same code as the diagram above, extra care has to be taken to prevent Mermaid from rendering it as a diagram.
Wrap it in Tera&#39;s &lt;a rel=&quot;external&quot; href=&quot;https://keats.github.io/tera/docs/#raw&quot;&gt;&lt;code&gt;raw&lt;/code&gt; block&lt;/a&gt; in the code block, so the code stays visible but is not executed.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span&gt;diagram&lt;/span&gt;&lt;span&gt; init&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;{&amp;#39;theme&amp;#39;: &amp;#39;forest&amp;#39;}&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; %&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1&quot;&gt;    CUSTOMER&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; ||&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;--&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; ORDER&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt; :&lt;/span&gt;&lt;span&gt; places&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1&quot;&gt;    ORDER&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; ||&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;|&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; LINEITEM&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt; :&lt;/span&gt;&lt;span&gt; contains&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1&quot;&gt;    PRODUCT&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; ||&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;--&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; LINEITEM&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt; :&lt;/span&gt;&lt;span&gt; is_listed_in&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1&quot;&gt;    CUSTOMER&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        string&lt;/span&gt;&lt;span&gt; Name&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        string&lt;/span&gt;&lt;span&gt; Email&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        string&lt;/span&gt;&lt;span&gt; Address&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1&quot;&gt;    PRODUCT&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        string&lt;/span&gt;&lt;span&gt; ProductName&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        float&lt;/span&gt;&lt;span&gt; Price&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1&quot;&gt;    ORDER&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        date&lt;/span&gt;&lt;span&gt; DateOrdered&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        string&lt;/span&gt;&lt;span&gt; Status&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-1&quot;&gt;    LINEITEM&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        int&lt;/span&gt;&lt;span&gt; Quantity&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;/&lt;/span&gt;&lt;span&gt;diagram&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; %&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Notes from Studying for the Professional ChromeOS Administrator Exam</title>
        <published>2023-09-15T00:00:00+00:00</published>
        <updated>2023-09-15T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/blog/chromeos-administrator/chromeos-administrator-exam-notes/"/>
        <id>/blog/chromeos-administrator/chromeos-administrator-exam-notes/</id>
        
        <content type="html" xml:base="/blog/chromeos-administrator/chromeos-administrator-exam-notes/">&lt;p&gt;I sat Google&#39;s &lt;strong&gt;Professional ChromeOS Administrator&lt;/strong&gt; certification in 2023. What follows started life as a scrappy set of study notes from that prep — a mix of things I&#39;d worked out myself, things pulled from Google&#39;s own documentation, and pointers to a handful of genuinely useful YouTube walkthroughs. I&#39;ve tidied them up here, roughly following the exam&#39;s own structure, partly so future-me has a cleaner reference, and partly on the off chance it&#39;s useful to anyone else sitting the same exam.&lt;/p&gt;
&lt;h2 id=&quot;section-1-admin-console-actions&quot;&gt;Section 1: Admin console actions&lt;/h2&gt;
&lt;h3 id=&quot;troubleshooting-and-day-to-day-admin-tasks&quot;&gt;Troubleshooting and day-to-day admin tasks&lt;/h3&gt;
&lt;p&gt;The bread-and-butter admin actions all live in fairly predictable places once you know where to look:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wiping devices&lt;/strong&gt;: &lt;code&gt;Devices → Chrome → Devices&lt;/code&gt; or &lt;code&gt;Devices → Overview → Mobile and Endpoints&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Collecting device logs&lt;/strong&gt;: pulled from the device itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raising tickets&lt;/strong&gt;: the question-mark icon, top right of the console.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connectivity issues&lt;/strong&gt;: &lt;code&gt;Devices → Networks&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;First-time user login problems&lt;/strong&gt;: usually comes down to Organisational Unit and user setup — &lt;code&gt;Users → Manage Organisational Units&lt;/code&gt; / &lt;code&gt;Add New User&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manually checking for updates&lt;/strong&gt;: &lt;code&gt;Devices → Chrome → Reports → Versions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feature requests&lt;/strong&gt;: the main menu&#39;s &quot;Send Feedback&quot; option.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Worth knowing about even if you never use them directly: the &lt;strong&gt;Trusted Tester Program&lt;/strong&gt;, and the fact that partners are informed of release notes separately from the standard release channel.&lt;/p&gt;
&lt;p&gt;For actual escalation to Google support: the support/help icon top-right, or &lt;a rel=&quot;external&quot; href=&quot;https://support.google.com/chrome/a&quot;&gt;support.google.com/chrome/a&lt;/a&gt; → &quot;Contact us&quot; → hardware, software/services, or troubleshooting. If self-service doesn&#39;t resolve it, &quot;This didn&#39;t help, continue to support&quot; gives you a phone option — you get a PIN valid for 60 minutes to call Google directly.&lt;/p&gt;
&lt;h3 id=&quot;update-channels-and-rollout&quot;&gt;Update channels and rollout&lt;/h3&gt;
&lt;p&gt;This is a section worth knowing cold, since it comes up in scenario questions a lot.&lt;/p&gt;
&lt;p&gt;Update settings live under &lt;code&gt;Devices → Chrome → Settings&lt;/code&gt; (on a child OU) → &lt;strong&gt;Device Update Settings → Auto Update Settings&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Channels&lt;/strong&gt;: Stable, Beta, Long Term Support, Long Term Support Candidate, Dev.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rollout schedule&lt;/strong&gt; options:
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Default&lt;/em&gt; — devices update as soon as a new version is available.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Scatter updates&lt;/em&gt; — useful for bandwidth-constrained fleets; spreads updates over up to two weeks.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Schedule&lt;/em&gt; — a staged rollout: update X% after Y days, then 100% after a further period.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blackout windows&lt;/strong&gt; stop automatic updates during specified times (manual updates are still possible).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auto-reboot behaviour&lt;/strong&gt; differs by session type: kiosk devices restart automatically after an update; user/managed guest sessions restart after the user&#39;s next sign-out. Both are configurable.&lt;/li&gt;
&lt;li&gt;A good chunk of a typical fleet — 5% is the figure to remember — should sit on the Beta channel, to catch problems before they hit Stable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One easy-to-miss detail: to stop an update being applied &lt;em&gt;before&lt;/em&gt; a device is even enrolled, press &lt;strong&gt;Ctrl+Alt+E&lt;/strong&gt; on the End User License Agreement screen. Skip this and a downloaded update that should have been blocked by policy can still apply itself on next restart.&lt;/p&gt;
&lt;p&gt;For diagnostics: the &lt;a rel=&quot;external&quot; href=&quot;https://toolbox.googleapps.com/apps/main/&quot;&gt;Log Analyzer&lt;/a&gt; tool, network logs at &lt;code&gt;/var/log&lt;/code&gt; (accessible via &lt;code&gt;file:///var/log&lt;/code&gt;), and &lt;code&gt;chrome://network&lt;/code&gt; for live inspection. Requesting exported logs needs the &lt;strong&gt;Export Logs Action&lt;/strong&gt; admin privilege specifically.&lt;/p&gt;
&lt;h3 id=&quot;wiping-deprovisioning-and-disabling-know-the-difference&quot;&gt;Wiping, deprovisioning and disabling — know the difference&lt;/h3&gt;
&lt;p&gt;These three get confused easily, and the exam leans on the distinction:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Action&lt;/th&gt;&lt;th&gt;What it does&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Clear user profiles&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Removes user profile data, keeps device policy and enrollment&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Factory reset&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Wipes everything, returns to factory state — unless the device is subject to automatic re-enrollment&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Deprovision&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Can be with or without a factory reset; requires a reason (e.g. retiring from fleet, ChromeOS Flex upgrade)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Disable&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Locks the device with a lock screen, optionally with factory reset&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;On-device, a factory reset is &lt;strong&gt;Ctrl+Alt+Shift+R&lt;/strong&gt;, then Restart → Powerwash → Continue. From the &lt;code&gt;Esc + Refresh + Power&lt;/code&gt; recovery screen you can also recover via internet connection, or press &lt;strong&gt;Ctrl+D&lt;/strong&gt; to enter developer mode (blocked if forced re-enrollment is set). On tablets, the equivalent is volume up + volume down + power.&lt;/p&gt;
&lt;h3 id=&quot;setting-up-a-trial&quot;&gt;Setting up a trial&lt;/h3&gt;
&lt;p&gt;Trials hinge on domain verification: &lt;code&gt;Home → Domains → Manage Domains → Add Domain&lt;/code&gt;, then verify ownership — either linking through your DNS provider directly, or adding a TXT record (&lt;code&gt;google-site-verification&lt;/code&gt;). Verification can take up to 12 hours, and every user on the domain gets a notification email once it completes.&lt;/p&gt;
&lt;p&gt;Before verification, the Chrome devices tile in the admin console sits greyed out. Once verified, the setup wizard walks you through DNS-provider-specific instructions, the tile unlocks, and you get access to Google support proper.&lt;/p&gt;
&lt;h3 id=&quot;pushing-applications&quot;&gt;Pushing applications&lt;/h3&gt;
&lt;p&gt;This is another heavily-tested area — pay close attention to &lt;em&gt;which&lt;/em&gt; method a question specifies (Chrome Web Store, Google Play Store, or App ID), and to any mention of installation policy or version pinning.&lt;/p&gt;
&lt;p&gt;Application settings live under &lt;code&gt;Devices → Chrome → Apps and Extensions&lt;/code&gt;, and apply to whichever OU you&#39;re configuring.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chrome extensions&lt;/strong&gt;: add from the Chrome Web Store. Default policy is &quot;allow installation&quot;; you can also force-install, or force-install &lt;em&gt;and&lt;/em&gt; pin to the shelf.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Android apps&lt;/strong&gt;: added via Google Play. Only apps approved in Play are available, same installation options apply, and there&#39;s a JSON configuration option for pre-setting app config.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Progressive Web Apps&lt;/strong&gt;: added by URL, which unlocks PWA features like real-time notifications and offline access, and can open in its own window for an app-like feel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OU-level policy&lt;/strong&gt;: &quot;Allow users to install other apps and extensions&quot; can be set to allow all, Play only, Chrome only, or block all others.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced settings&lt;/strong&gt; (gear icon): granular allow/block per app, and permission-based blocking of extensions — e.g. blocking anything that requests video capture.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;device-actions-and-licensing&quot;&gt;Device actions and licensing&lt;/h3&gt;
&lt;p&gt;Most per-device actions (gathering logs, deprovisioning, moving OU, remote desktop, screenshots, resetting profiles) live under &lt;code&gt;Devices → Chrome → Devices → [select device]&lt;/code&gt;. &lt;strong&gt;Zero-Touch Enrollment&lt;/strong&gt; tokens come from the same area, scoped to an OU: generate a 30-character pre-provisioning token, which you can copy, revoke, or regenerate. Devices show as &quot;pre-provisioned&quot; until they check in. ZTE itself needs a capable device, an approved manufacturer that&#39;s pre-enrolled it, and a partner relationship.&lt;/p&gt;
&lt;p&gt;On licensing, the distinction that matters is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bundled/perpetual license&lt;/strong&gt; — tied to the device for its lifetime, non-transferable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chrome Education Upgrade&lt;/strong&gt; — purchased separately, transferable to another device of the same model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Annual licenses&lt;/strong&gt; — bought per term (e.g. for kiosk/signage use), also transferable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Devices must be enrolled &lt;em&gt;before&lt;/em&gt; anyone signs in — if that&#39;s missed, the only fix is wiping and restarting enrollment. A summary of assigned vs. purchased licenses lives under &lt;code&gt;Devices → Chrome → Devices → Upgrades&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;admin-roles&quot;&gt;Admin roles&lt;/h3&gt;
&lt;p&gt;Custom roles are created under &lt;code&gt;Accounts → Admin Roles&lt;/code&gt;, then applied to a user via &lt;code&gt;Users → [select user] → Admin Roles and Privileges → Assign roles&lt;/code&gt;. Roles can be scoped to a specific OU — with one important exception: the &lt;strong&gt;support ticket&lt;/strong&gt; privilege is organisation-wide and can&#39;t be OU-scoped. Don&#39;t mix OU-scoped and organisation-wide permissions on the same custom role.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Super Admin&lt;/strong&gt; is a separate tier entirely: full access to everything, must be a native Google identity (not usable with SSO), and best practice is to create super admin accounts with distinct UPNs from everyday admin accounts. Certain actions — billing, accepting terms and conditions — require super admin specifically.&lt;/p&gt;
&lt;h2 id=&quot;section-2-security-processes&quot;&gt;Section 2: Security processes&lt;/h2&gt;
&lt;h3 id=&quot;certificates-and-vpn&quot;&gt;Certificates and VPN&lt;/h3&gt;
&lt;p&gt;Certificate deployment lives under &lt;code&gt;Devices → Networks → Certificates&lt;/code&gt;. For automatic user/device certificate provisioning via &lt;strong&gt;SCEP&lt;/strong&gt; (Simple Certificate Enrollment Protocol): select the OU, upload a certificate authority, then create a SCEP profile — profile name, fully distinguished name, common name (&lt;code&gt;${USER_EMAIL}&lt;/code&gt;), subject alternative name, SCEP server URL, extended key usage (client and/or server authentication), and usually a static challenge.&lt;/p&gt;
&lt;p&gt;Wiring that certificate into a Wi-Fi network: create the network under Devices → Networks, choose 802.1X with EAP-TLS, set the username field to &lt;code&gt;${LOGIN_ID}&lt;/code&gt;, and select the SCEP profile and root cert you just created.&lt;/p&gt;
&lt;p&gt;Android VPN requires the &quot;Advanced&quot; tier of mobile management (toggled under &lt;code&gt;Mobiles and Endpoints → Settings → Universal&lt;/code&gt;), and requires users to install an app to use it.&lt;/p&gt;
&lt;h3 id=&quot;core-security-tenets&quot;&gt;Core security tenets&lt;/h3&gt;
&lt;p&gt;A few things worth internalising rather than memorising as menu paths:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No antivirus needed&lt;/strong&gt; — every page and app runs sandboxed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verified boot&lt;/strong&gt; — every startup runs an integrity self-check; if tampering or corruption is detected, ChromeOS attempts to repair and restore itself automatically.&lt;/li&gt;
&lt;li&gt;Client-side encryption settings live under &lt;code&gt;Security → Access and Data Control&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Forced re-enrollment and Verified Access both sit under &lt;code&gt;Devices → Chrome → Settings → Device&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;section-3-chromeos-policies&quot;&gt;Section 3: ChromeOS policies&lt;/h2&gt;
&lt;p&gt;Enrollment permissions are set through Chrome user/browser policies rather than an admin privilege as such: &lt;code&gt;Users → [user] → Chrome User Policies → Devices → User and Browser Settings&lt;/code&gt;, then search &quot;Enrollment.&quot; From there you can force devices into a specific enrollment OU, or lock enrollment down entirely at the top-level OU.&lt;/p&gt;
&lt;p&gt;The policy surface itself is large. The practically useful thing to know going in is &lt;em&gt;where&lt;/em&gt; a given policy lives — device-level, user-level, or specifically for kiosk mode — since kiosk-related questions came up a lot when I sat the exam. If you&#39;ve never had to configure kiosk mode for real, it&#39;s worth deliberately spending time there rather than assuming general ChromeOS familiarity will cover it.&lt;/p&gt;
&lt;p&gt;One small shortcut worth remembering: &lt;strong&gt;Alt+V&lt;/strong&gt; shows system info on the sign-in screen.&lt;/p&gt;
&lt;h2 id=&quot;section-4-identity-management&quot;&gt;Section 4: Identity management&lt;/h2&gt;
&lt;p&gt;New users are created under &lt;code&gt;Directory → Users → Add New&lt;/code&gt; — name, auto-generated username, optional photo, OU assignment, optional secondary email for sending credentials, and an 8-character minimum password. Bulk creation uses a CSV template: first name, last name, email, password, and OU path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Setting up SSO&lt;/strong&gt; is a longer sequence worth having as a checklist:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Configure the identity provider (SAML or OIDC).&lt;/li&gt;
&lt;li&gt;Enable SAML-based SSO under &lt;code&gt;Devices → Chrome → User &amp;amp; Browser Settings → Security&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enable transfer of SAML SSO cookies into the user session at sign-in, if wanted.&lt;/li&gt;
&lt;li&gt;Optionally keep local ChromeOS and SAML passwords in sync — choosing whether to enforce online sign-in at the login screen only, or both login and lock screen.&lt;/li&gt;
&lt;li&gt;Set up password expiration notifications via the &lt;code&gt;passwordExpirationTimestamp&lt;/code&gt; attribute.&lt;/li&gt;
&lt;li&gt;Enable IdP redirection, so users go straight to the SAML page rather than seeing a Google-branded intermediate screen.&lt;/li&gt;
&lt;li&gt;Enable username passthrough, so the username auto-fills on the IdP&#39;s login page.&lt;/li&gt;
&lt;li&gt;Control what&#39;s reachable from the sign-in/lock screen via blocked URLs and exceptions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Third-party IdP configuration (sign-in URL, sign-out URL, certificate, change-password URL) lives under &lt;code&gt;Security → Authentication → SSO with third-party IdP&lt;/code&gt;. Domain-specific service URLs can either redirect straight to the third-party IdP or prompt for a Google username.&lt;/p&gt;
&lt;h2 id=&quot;section-5-general-tenets-and-hardware&quot;&gt;Section 5: General tenets and hardware&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Test domains&lt;/strong&gt;: added as a secondary domain under &lt;code&gt;Domains&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OS update cadence&lt;/strong&gt;: new milestone releases roughly every 4 weeks; Long Term Support gets milestone updates every 6 months.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Password manager policy&lt;/strong&gt;: &lt;code&gt;Devices → Chrome → User and Browser Settings&lt;/code&gt;, search &quot;Security&quot; → Password Manager, with an option to disable it outright.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lock screen&lt;/strong&gt;: configurable idle timeout before the screen locks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keeping devices managed&lt;/strong&gt;: forced re-enrollment (&lt;code&gt;Devices → Chrome → Device Settings → Enrollment and Access&lt;/code&gt;) is on by default, and pairs with requiring verified boot mode for Verified Access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disabling a device&lt;/strong&gt;: &lt;code&gt;Devices → [select device] → Disable&lt;/code&gt;, with an optional custom message, and it&#39;s reversible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On hardware selection, the exam expects general awareness of ChromeOS device specs, hardware capability tiers, and &lt;strong&gt;ChromeOS Flex&lt;/strong&gt; as a route for repurposing existing (non-Chromebook) hardware — rather than deep specifics.&lt;/p&gt;
&lt;h2 id=&quot;a-few-loose-scenario-notes&quot;&gt;A few loose scenario notes&lt;/h2&gt;
&lt;p&gt;A couple of specific scenario-style points that stuck with me from prep:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Migrating email from IMAP into Google Workspace: the correct tool is Google&#39;s native &lt;strong&gt;Data Migration&lt;/strong&gt; service, not a manual export/import.&lt;/li&gt;
&lt;li&gt;Within the security investigation tool, one of the available actions on a user in the search results is &quot;Restore user.&quot;&lt;/li&gt;
&lt;li&gt;Manual enrollment shortcut: &lt;strong&gt;Ctrl+Alt+E&lt;/strong&gt; on the sign-in/enrollment screen.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;further-resources&quot;&gt;Further resources&lt;/h2&gt;
&lt;p&gt;A handful of the videos and docs I found genuinely useful while studying:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google&#39;s own support hub: &lt;a rel=&quot;external&quot; href=&quot;https://support.google.com/chrome/a&quot;&gt;support.google.com/chrome/a&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://support.google.com/chrome/a/topic/6239504&quot;&gt;ChromeOS device wipe and reset options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://support.google.com/chrome/a/answer/9036081&quot;&gt;Update channels and auto-update settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://support.google.com/chrome/a/answer/2984343&quot;&gt;Admin roles and privileges&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://support.google.com/chrome/a/topic/9028719&quot;&gt;SCEP certificate deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://support.google.com/a/answer/12032922&quot;&gt;Setting up SSO for Chrome devices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://support.google.com/chrome/a/answer/6060880&quot;&gt;Creating and managing users&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Internal rate of return calculation</title>
        <published>2023-07-29T00:00:00+00:00</published>
        <updated>2023-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/cms/calculating-internal-interest-rate/"/>
        <id>/pages/cms/calculating-internal-interest-rate/</id>
        
        <content type="html" xml:base="/pages/cms/calculating-internal-interest-rate/">&lt;p&gt;The internal rate of return (IRR) is a potent financial measure used in financial analysis, accounting, and portfolio management to gauge the profitability of investments.
It&#39;s like the heartbeat of an investment - an indicator of health, the higher it is, the better the investment.
This powerful tool can help investors compare different investment options, assisting in making informed decisions about where to allocate their capital.
Especially when cash flows are irregular, as often is the case in real-world scenarios, IRR becomes invaluable.
This article will first explain the concept and calculations behind IRR and then demonstrate how to implement an IRR algorithm in Rust.&lt;/p&gt;
&lt;h2 id=&quot;irr-vs-xirr-terminology&quot;&gt;IRR vs. XIRR terminology&lt;/h2&gt;
&lt;p&gt;The IRR is the rate of return that makes the net present value (NPV) of all cash flows (both positive and negative) from a particular investment equal to zero.
It&#39;s sometimes used for cash flows at regular, usually annual, intervals.
I will not make this assumption in this article.
While Microsoft uses the name XIRR for its Excel function, I will use the term IRR throughout this article, even though the cash flows do not occur at regular intervals.&lt;/p&gt;
&lt;h2 id=&quot;example-data-and-validation&quot;&gt;Example data and validation&lt;/h2&gt;
&lt;p&gt;A crucial part of implementing a financial algorithm like IRR is validation. That is, making sure that your algorithm is producing correct and expected results.
One effective way to validate your implementation is by comparing its outputs to those of &lt;a rel=&quot;external&quot; href=&quot;https://support.microsoft.com/en-gb/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d&quot;&gt;Microsoft Excel&#39;s XIRR function&lt;/a&gt;.
I will use the data from the Excel example.
Using Excel&#39;s XIRR function to compute the internal rate of return for this specific data set yields a result of &lt;span class=&quot;math math-inline&quot; role=&quot;math&quot;&gt;\(37.34\%\)&lt;/span&gt;.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: right&quot;&gt;$\textbf{i}$&lt;/th&gt;&lt;th&gt;ISO Date $\textbf{d}$&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Years since first cash flow $\textbf{y}$&lt;/th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Payment $\textbf{p}$&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;1&lt;/td&gt;&lt;td&gt;2008-01-01&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;0.0000&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;-10,000&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;&lt;td&gt;2008-03-01&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;0.1644&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;2,750&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;3&lt;/td&gt;&lt;td&gt;2008-10-30&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;0.8301&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;4,250&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;4&lt;/td&gt;&lt;td&gt;2009-02-15&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1.1260&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;3,250&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;5&lt;/td&gt;&lt;td&gt;2009-04-01&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1.2493&lt;/td&gt;&lt;td style=&quot;text-align: right&quot;&gt;2,750&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;how-irr-works&quot;&gt;How IRR works&lt;/h2&gt;
&lt;p&gt;The IRR is calculated by finding the rate of return that would make the net present value (NPV) of an investment equal to zero.
The NPV is the sum of the present values of all future cash flows from an investment.
The derivative of the NPV is used to find the IRR.
The derivative of the NPV is the rate of change of the NPV with respect to the interest rate.&lt;/p&gt;
&lt;p&gt;The IRR algorithm is an instance of the &lt;a rel=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Newton%27s_method&quot;&gt;Newton-Raphson method&lt;/a&gt;,
which is a root-finding algorithm that produces successively better approximations to the roots (or zeroes) of a real-valued function.
To calculate the IRR, you need an array with pairs of data: one is the date of a cash flow, and the other is its payment amount.
You also need an initial guess for the IRR rate, which is often set as &lt;span class=&quot;math math-inline&quot; role=&quot;math&quot;&gt;\(10\%\)&lt;/span&gt; or $0.1$.&lt;/p&gt;
&lt;p&gt;Here&#39;s the general process of the algorithm:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Compute the net present value (NPV)&lt;/strong&gt;: The NPV is calculated by summing the present values of the individual cash flows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute the derivative of the NPV&lt;/strong&gt;: The derivative of the NPV with respect to the rate is created by differentiating the NPV formula and summing these values.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Update the guess&lt;/strong&gt;: The next guess for the rate is calculated by subtracting the ratio of the NPV to its derivative from the current guess.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iterate until convergence&lt;/strong&gt;: These steps are repeated until the change in guesses is below a certain tolerance or until a maximum number of iterations is reached.
At this point, the guess for the rate is considered to be the IRR.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;compute-the-net-present-value-npv&quot;&gt;Compute the net present value (NPV)&lt;/h2&gt;
&lt;p&gt;Here, $y_i$ represents the number of years that have passed since the first cash flow,
calculated as the difference in days between the date of the $i$-th cash flow $d_i$ and the date of the first cash flow $d_1$,
divided by 365 (days in one year).
I have pre-calculated it in the table above:&lt;/p&gt;
&lt;p&gt;$$y_i = \frac{d_i - d_1}{365}$$&lt;/p&gt;
&lt;p&gt;The NPV is the sum of all payments, where the correct interest rate $irr$ is applied.
Its value is 0 after all transactions have been concluded:&lt;/p&gt;
&lt;p&gt;$$\mathit{NPV} = \sum_{i = 1}^{m} \frac{p_i}{(1 + irr)^{y_i}}$$&lt;/p&gt;
&lt;p&gt;When you &lt;a rel=&quot;external&quot; href=&quot;https://www.wolframalpha.com/input?i=plot+-10000*%281%2F%281+%2B+x%29%5E%280.0000%29%29+%2B+2750*%281%2F%281+%2B+x%29%5E%280.1644%29%29+%2B+4250*%281%2F%281+%2B+x%29%5E%280.8301%29%29+%2B+3250*%281%2F%281+%2B+x%29%5E%281.1260%29%29+%2B+2750*%281%2F%281+%2B+x%29%5E%281.2493%29%29+from+x%3D-0.1+to+0.9&quot;&gt;plot&lt;/a&gt; the NPV function as a function of $irr$,
you are essentially varying the interest rate and observing how it affects the NPV.
Here&#39;s what you see on the graph below, which plots the NPV as a function of the rate with the values from the table above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;X-axis&lt;/strong&gt;: The x-axis represents the interest rate $irr$.
It typically ranges from &lt;span class=&quot;math math-inline&quot; role=&quot;math&quot;&gt;\(0\%\)&lt;/span&gt; to a reasonable upper limit, depending on the context of your problem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Y-axis&lt;/strong&gt;: The y-axis represents the net present value NPV.
It&#39;s the cumulative sum of the discounted cash flows according to the given formula.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shape of the curve&lt;/strong&gt;: The curve of the NPV function will generally be downward-sloping.
This is because as you increase the interest rate $irr$, the present value of future cash flows decreases.
Higher interest rates mean that future cash flows are being discounted more heavily, which reduces their present value.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Break-even point&lt;/strong&gt;: There will be a point on the graph where the NPV curve intersects the x-axis.
It&#39;s where our $irr$ satisfies the condition $\mathit{NPV} = 0$.
For our data, it is somewhere between $0.35$ and $0.40$, or &lt;span class=&quot;math math-inline&quot; role=&quot;math&quot;&gt;\(35\%\)&lt;/span&gt; and &lt;span class=&quot;math math-inline&quot; role=&quot;math&quot;&gt;\(40\%\)&lt;/span&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;mt-4&quot;&gt;&lt;span class=&quot;light-dark-image inline-block&quot;&gt;
      &lt;img class=&quot;block image-shortcode&quot; src=&quot;/pages/cms/calculating-internal-interest-rate/irr-npv-function-light.webp&quot; alt=&quot;Plot of the NPV as a function of the rate with the values from the table&quot; width=&quot;560.0&quot; height=&quot;360.0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
      &lt;img class=&quot;block image-shortcode&quot; src=&quot;/pages/cms/calculating-internal-interest-rate/irr-npv-function-dark.webp&quot; alt=&quot;Plot of the NPV as a function of the rate with the values from the table&quot; width=&quot;560.0&quot; height=&quot;360.0&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;italic&quot;&gt;Downward-sloping NPV curve&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The above formula is a sum of function terms.
Applying the &lt;a rel=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Superposition_principle&quot;&gt;superposition principle&lt;/a&gt;,
we can split the sum into individual function terms, which look all the same.
For each cash flow, we have a function term, which is the present value of the cash flow.
The important part is that we can apply this principle to the NPV function &lt;strong&gt;and&lt;/strong&gt; the derivative of the NPV function.&lt;/p&gt;
&lt;p&gt;With $irr$ written as $x$, each function term looks like this, where $p_i$ and $y_i$ are just constants.
$f_i(x)$ represents the NPV for a single cash flow, the $i$-th one:&lt;/p&gt;
&lt;p&gt;$$f_i(x) = \frac{p_i}{(1 + x)^{y_i}}$$&lt;/p&gt;
&lt;p&gt;The amount $p_i$ is discounted in the NPV calculation to reflect the time value of money. Here&#39;s an explanation for why that&#39;s done:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time value of money&lt;/strong&gt;: Money available today is worth more than the same amount in the future because of its potential earning capacity.
This core principle of finance holds that, provided money can earn interest, any amount of money is worth more the sooner it is received.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discounting future cash flows&lt;/strong&gt;: In the context of the internal rate of return (IRR) and net present value (NPV), this principle is applied to future cash flows.
The $p_i$ represents a payment (cash flow) at some future date. To compare this future value to present values,
it needs to be discounted back to its value in today&#39;s terms.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The discount factor&lt;/strong&gt;: The expression $\frac{1}{(1 + x)^{y_i}}$ serves as a discount factor, where $x$ is the internal rate of return, and $y_i$ is the number of years that have passed since the first cash flow.
This factor is derived from the formula for compounding interest in reverse (discounting rather than compounding).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sum of discounted payments&lt;/strong&gt;: The NPV is the sum of these discounted payments. By discounting each future cash flow back to its present value,
the NPV provides a consistent basis for comparing the value of cash flows across different time periods.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Understanding the equation&lt;/strong&gt;: The equation $f_i(x) = \frac{p_i}{(1 + x)^{y_i}}$ represents the present value of the $i$-th cash flow.
It shows how each cash flow is discounted back to present value terms using the discount factor.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Discounting the $p_i$ in the NPV calculation allows for a proper comparison of cash flows across different time periods,
taking into account the time value of money and the specific internal rate of return (IRR) required from the investment.
It ensures that future cash flows are appropriately weighed against the potential earning capacity of money in the present.&lt;/p&gt;
&lt;h3 id=&quot;but-what-happens-if&quot;&gt;But what happens if ...&lt;/h3&gt;
&lt;p&gt;The expression $(1 + x)^{y_i}$ can become a problem if $x &amp;lt; -1$ (less than &lt;span class=&quot;math math-inline&quot; role=&quot;math&quot;&gt;\(-100\%\)&lt;/span&gt;), because raising a negative number to a non-integer power will result in a complex number.
Within the realm of real numbers, this operation is undefined.
For example, consider a case where $x = -1.1$ and $y_i$ is a floating point number like $0.4$.
Then, $(1 + x)$ will be negative, and raising it to the $y_i$ power &lt;a rel=&quot;external&quot; href=&quot;https://www.wolframalpha.com/input?i=%281+-+1.1%29%5E%280.4%29&quot;&gt;produces a complex value&lt;/a&gt;:
${(1 - 1.1)}^{0.4} \approx 0.12 + 0.38 i$&lt;/p&gt;
&lt;p&gt;In the context of finance and the calculation of IRR, it doesn&#39;t make sense to have complex numbers as they don&#39;t have a practical interpretation in terms of cash flows or discount rates.
Therefore, you would typically ensure that the rate $x$ stays within the range where the calculation remains within the realm of real numbers.
If you&#39;re working with cash flows and discount rates, you may want to either avoid or handle situations where $x &amp;lt; -1$ by constraining the value of $x$ within a reasonable range or taking some other specific action if $x$ falls outside that range.&lt;/p&gt;
&lt;p&gt;In practice, it&#39;s rare that you would come across this scenario.&lt;/p&gt;
&lt;h2 id=&quot;compute-the-derivative-of-the-npv&quot;&gt;Compute the derivative of the NPV&lt;/h2&gt;
&lt;p&gt;We can now apply the &lt;a rel=&quot;external&quot; href=&quot;https://www.wolframalpha.com/input?i=derivative+of+a+%2F+%28%281+%2B+x%29%5Eb%29&quot;&gt;basic differentiation rules&lt;/a&gt; to get the derivative of $f_i(x)$.
It represents the derivative of the NPV for a single cash flow, also the $i$-th one:&lt;/p&gt;
&lt;p&gt;$$f_i^\prime(x) = \frac{-y_i \cdot p_i}{(1 + x)^{y_i + 1}}$$&lt;/p&gt;
&lt;h2 id=&quot;update-the-guess&quot;&gt;Update the guess&lt;/h2&gt;
&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Newton%27s_method&quot;&gt;Newton-Raphson method&lt;/a&gt; is an iterative process used to locate the roots of a differentiable function, i.e. a solution for $x$ where $f(x) = 0$.
The method begins with an initial estimate $x_n$, which is then refined into a more accurate guess, $x_{n+1}$.
This stage is where the NPV and its derivative come into play.
Instead of solely employing $f_i(x_n)$ and $f_i^\prime(x_n)$ for a single cash flow, we incorporate all the cash flows by adding up the NPV terms and derivative terms respectively:&lt;/p&gt;
&lt;p&gt;$$x_{n+1} = x_{n} - \frac{\sum_{i = 1}^{m} f_i(x_n)}{\sum_{i = 1}^{m} f_i^\prime(x_n)} $$&lt;/p&gt;
&lt;h2 id=&quot;iterate-until-convergence&quot;&gt;Iterate until convergence&lt;/h2&gt;
&lt;p&gt;Instead of improving the result indefinitely, you stop when the difference between $x_n$ and $x_{n+1}$ is smaller than or equal to a given $\epsilon$, i.e. $\left| x_n - x_{n+1} \right| \le \epsilon$.
It&#39;s usually a tiny number, like $0.000001$ ($1.0 e^{-6}$).
This is the value used in Microsoft Excel&#39;s XIRR function.&lt;/p&gt;
&lt;p&gt;It&#39;s also worth noting that despite using the exact derivative, the IRR algorithm is still an approximation method.
It may not always converge to a solution, especially for cash flow series that have multiple changes in sign.
In these cases, it may be necessary to provide a good initial guess for the rate or to use a different method to compute the internal rate of return.&lt;/p&gt;
&lt;h2 id=&quot;implementation-in-rust&quot;&gt;Implementation in Rust&lt;/h2&gt;
&lt;p&gt;Let&#39;s move towards translating our theoretical understanding into a concrete implementation using Rust.
We&#39;ll focus on implementing the IRR calculation algorithm by applying the findings and formulas mentioned in the previous sections.
The final output should be close to Excel&#39;s XIRR function result within an acceptable tolerance range.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; We use the chrono crate for date handling: https://docs.rs/chrono/latest/chrono&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; chrono&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;prelude&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;*&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Function to calculate the number of days between two dates&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; days_between&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;start_date&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; NaiveDate&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; end_date&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; NaiveDate&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; i64&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Calculate the duration between the two dates&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    let&lt;/span&gt;&lt;span&gt; duration&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; end_date&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;signed_duration_since&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;start_date&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Return the duration in days&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    duration&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;num_days&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Main function to calculate the internal rate of return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; calculate_irr&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;transactions&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;amp;&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;f64&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; f64&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; initial_guess&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; f64&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; f64&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Start with initial guess for the IRR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    let&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; mut&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; initial_guess&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    const&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-7&quot;&gt; MAX_TRIES&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; usize&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 100&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    const&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-7&quot;&gt; EPSILON&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; f64&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-9&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;6&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Loop until a maximum number of iterations is reached&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    for&lt;/span&gt;&lt;span&gt; _&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; in&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-7&quot;&gt;MAX_TRIES&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;        //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Calculate NPV and its derivative for each cash flow and sum them up&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;        let&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;fx&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; dfx&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; transactions&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;iter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;            .&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;|&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;years&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; amount&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;|&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;                let&lt;/span&gt;&lt;span&gt; fx&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; amount&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; /&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;1_&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;f64&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;powf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;*&lt;/span&gt;&lt;span&gt;years&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;                  //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; NPV of the cash flow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;                let&lt;/span&gt;&lt;span&gt; dfx&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;-&lt;/span&gt;&lt;span&gt;years&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span&gt; amount&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; /&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;1_&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;f64&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;powf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;years&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-9&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Derivative of NPV&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                (&lt;/span&gt;&lt;span&gt;fx&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; dfx&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;                                                    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Return both values for a single cash flow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;            //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Sum up all NPVs and derivatives for all cash flows (reduce to a tuple of two sums)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;            .&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;fold&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;0_&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;f64&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0_&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;f64&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; |&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fx_acc&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; dfx_acc&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;fx&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; dfx&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;|&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;fx_acc&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span&gt; fx&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; dfx_acc&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span&gt; dfx&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;        //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Calculate the new guess&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;        let&lt;/span&gt;&lt;span&gt; new_x&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span&gt; fx&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; /&lt;/span&gt;&lt;span&gt; dfx&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;        //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Calculate the difference between the new and the old guess&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;        let&lt;/span&gt;&lt;span&gt; epsilon&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;new_x&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;abs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;        //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; If the difference is smaller than or equal to the tolerance, we are done&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;        if&lt;/span&gt;&lt;span&gt; epsilon&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;=&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-7&quot;&gt; EPSILON&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;            return&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;        //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Continue the loop with the new guess&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        x&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; new_x&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Return a non-number if the algorithm didn&amp;#39;t converge (see &amp;quot;Iterate until convergence&amp;quot; in article)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;    f64&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-7&quot;&gt;NAN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Define the main function and our example transactions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Example from Microsoft Excel documentation:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; https://support.microsoft.com/en-gb/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; The internal rate of return is 0.373362535 or 37.34%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    let&lt;/span&gt;&lt;span&gt; cashflows&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; vec!&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;NaiveDate&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;from_ymd_opt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;2008&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;  1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;  1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;unwrap&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;10_000&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-9&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;00&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;NaiveDate&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;from_ymd_opt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;2008&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;  3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;  1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;unwrap&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;   2_750&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-9&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;00&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;NaiveDate&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;from_ymd_opt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;2008&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 30&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;unwrap&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;   4_250&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-9&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;00&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;NaiveDate&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;from_ymd_opt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;2009&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;  2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 15&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;unwrap&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;   3_250&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-9&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;00&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;NaiveDate&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;from_ymd_opt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;2009&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;  4&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;  1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;unwrap&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;   2_750&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-9&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;00&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Pre-calculate the years since first cash flow for each cash flow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    let&lt;/span&gt;&lt;span&gt; transactions&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; Vec&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt;f64&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; f64&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; cashflows&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;iter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;        .&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;|&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; amount&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;|&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;days_between&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cashflows&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; as&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; f64&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; /&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 365&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-9&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span&gt;amount&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;        .&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;collect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Call our IRR calculation function and print the result&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Expected result is 37.34%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;    println!&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;IRR with Newton-Raphson: &lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;:.2&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; calculate_irr&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;transactions&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-9&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 100&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-9&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;calculate_irr&lt;/code&gt; function is where the heart of our IRR calculation lives.
It starts by defining the initial guess for the IRR, then enters a loop that calculates the new guess, checks for convergence, and continues the loop if not converged.
Finally, if the algorithm fails to converge, it returns a non-number.
The main function uses an example transaction set and calls our &lt;code&gt;calculate_irr&lt;/code&gt; function.&lt;/p&gt;
&lt;h2 id=&quot;bonus-implementation-in-javascript&quot;&gt;Bonus: implementation in JavaScript&lt;/h2&gt;
&lt;p&gt;JavaScript runs natively in all modern browsers and is the &lt;em&gt;lingua franca&lt;/em&gt; of the web.
By translating the Rust code into JavaScript, the IRR calculation will be easily usable on virtually any device, regardless of operating system or hardware.
This way, it should be possible to use it in a web application or even in a mobile app using a framework like &lt;a rel=&quot;external&quot; href=&quot;https://reactnative.dev&quot;&gt;React Native&lt;/a&gt;.
Maybe you want to write a financial app that runs on iOS, Android, and the web?&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Function to calculate the number of days between two dates&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; daysBetween&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt; end&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  const&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; millisecondsPerDay&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1000&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 60&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 60&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 24&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  const&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; millisBetween&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; end&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;getTime&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span&gt; start&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;getTime&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  return&lt;/span&gt;&lt;span&gt; millisBetween&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; /&lt;/span&gt;&lt;span&gt; millisecondsPerDay&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Main function to calculate the internal rate of return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; calculateIRR&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;transactions&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt; initialGuess&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;  //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Start with initial guess for the IRR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  let&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; initialGuess&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  const&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; MAX_TRIES&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 100&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  const&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; EPSILON&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;0e-6&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;  //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Loop until a maximum number of iterations is reached&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;let&lt;/span&gt;&lt;span&gt; tries&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;span&gt; tries&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; MAX_TRIES&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;span&gt; tries&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +=&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Calculate NPV and its derivative for each cash flow and sum them up&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    const&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; fxs&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; dfxs&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; transactions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-d-16&quot;&gt;      .&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;tx&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;        const&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; fx&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; tx&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;amount&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; /&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;*&lt;/span&gt;&lt;span&gt; tx&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;years&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;                      //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; NPV of the cash flow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;        const&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; dfx&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;-&lt;/span&gt;&lt;span&gt;tx&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;years&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span&gt; tx&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;amount&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; /&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;*&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;tx&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;years&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Derivative of NPV&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;        return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt; fx&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; dfx&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;                                                  //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Return both values for a single cash flow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;      //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Sum up all NPVs and derivatives for all cash flows (reduce to an object with two sums)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-d-16&quot;&gt;      .&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;reduce&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt; obj&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt; fxs&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span&gt; acc&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;fxs&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span&gt; obj&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;fx&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; dfxs&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span&gt; acc&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;dfxs&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span&gt; obj&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;dfx&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt; fxs&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; dfxs&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Calculate the new guess&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    const&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; xNew&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span&gt; fxs&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; /&lt;/span&gt;&lt;span&gt; dfxs&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Calculate the difference between the new and the old guess&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    const&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; epsilon&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; Math&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;abs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;xNew&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; If the difference is smaller than or equal to the tolerance, we are done&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;epsilon&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;=&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; EPSILON&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;      return&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;    //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Continue the loop with the new guess&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    x&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; xNew&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;  //&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Return a non-number if the algorithm didn&amp;#39;t converge&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  return&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; NaN&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Example from Microsoft Excel documentation:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; https://support.microsoft.com/en-gb/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; The internal rate of return is 0.373362535 or 37.34%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; cashflows&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  {&lt;/span&gt;&lt;span&gt; date&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; new&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;2008-01-01&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; amount&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;00&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  {&lt;/span&gt;&lt;span&gt; date&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; new&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;2008-03-01&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; amount&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;   2750&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;00&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  {&lt;/span&gt;&lt;span&gt; date&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; new&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;2008-10-30&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; amount&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;   4250&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;00&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  {&lt;/span&gt;&lt;span&gt; date&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; new&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;2009-02-15&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; amount&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;   3250&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;00&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  {&lt;/span&gt;&lt;span&gt; date&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; new&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;2009-04-01&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; amount&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;   2750&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;00&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;]&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Pre-calculate the years since first cash flow for each cash flow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; transactions&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; cashflows&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-d-16&quot;&gt;  .&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;cf&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt; years&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; daysBetween&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cashflows&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; cf&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; /&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 365&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; amount&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;:&lt;/span&gt;&lt;span&gt; cf&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span&gt;amount&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Call our IRR calculation function and print the result&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Expected result is 37.34%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;console&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;IRR with Newton-Raphson: &lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-17&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;z-l-2&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;calculateIRR&lt;/span&gt;&lt;span class=&quot;z-l-2&quot;&gt;(&lt;/span&gt;&lt;span&gt;transactions&lt;/span&gt;&lt;span class=&quot;z-l-2&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 100&lt;/span&gt;&lt;span class=&quot;z-l-2&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;toFixed&lt;/span&gt;&lt;span class=&quot;z-l-2&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;z-l-2&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-17&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;z-l-2 z-d-8&quot;&gt;`&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The internal rate of return (IRR) is an instrumental financial measure that helps investors compare and decide between different investment opportunities.
Calculating it can get complex due to irregular cash flows, making a detailed understanding of the underlying algorithm crucial.
This article dived into the mathematics behind IRR calculation and provided a concrete implementation using the Rust programming language.
Remember, IRR is not just a number but an insight into the potential of an investment, assisting you in making informed financial decisions.&lt;/p&gt;
&lt;p&gt;If you have any questions, need further clarification, or want to give feedback: please let me know.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The Game of Fifteen</title>
        <published>2020-06-11T00:00:00+00:00</published>
        <updated>2020-06-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            Thomas Weitzel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/cms/game-of-fifteen/"/>
        <id>/pages/cms/game-of-fifteen/</id>
        
        <content type="html" xml:base="/pages/cms/game-of-fifteen/">&lt;p&gt;The Game of Fifteen is a sliding puzzle that consists of a square with tiles numbered from 1 to 15 in random order with one tile missing.
To solve the puzzle, you must place the tiles in order by moving tiles to the empty space.&lt;/p&gt;
&lt;h2 id=&quot;how-to-play&quot;&gt;How to play&lt;/h2&gt;
&lt;p&gt;Play the game online by clicking/tapping on the tile you want to move.
Only tiles neighboring the empty space can be moved.
Once you solve the puzzle, the tiles will not move anymore.
Just play a new game by clicking the button.
I can guarantee you that every game presented here is solvable.
Read below for an explanation.&lt;/p&gt;
&lt;p&gt;Hint: if your browser doesn&#39;t display the board correctly, try clearing your browser cache and try again.&lt;/p&gt;
&lt;div id=&quot;board&quot;&gt;
    &lt;div class=&quot;m-4 grid grid-flow-row grid-cols-4 grid-rows-4 h-64 w-64 gap-1&quot;&gt;
        &lt;div id=&quot;f0&quot;  class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f1&quot;  class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f2&quot;  class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f3&quot;  class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f4&quot;  class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f5&quot;  class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f6&quot;  class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f7&quot;  class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f8&quot;  class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f9&quot;  class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f10&quot; class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f11&quot; class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f12&quot; class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f13&quot; class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f14&quot; class=&quot;&quot;&gt;&lt;/div&gt;
        &lt;div id=&quot;f15&quot; class=&quot;&quot;&gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;mt-4&quot;&gt;
    &lt;button id=&quot;playButton&quot; class=&quot;py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-neutral-950 bg-primary-300 shadow-sm hover:bg-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-neutral-950 active:bg-primary-600 transition duration-150 ease-in-out&quot;&gt;
        new game
    &lt;/button&gt;
&lt;/div&gt;
&lt;h2 id=&quot;parity-of-a-permutation&quot;&gt;Parity of a permutation&lt;/h2&gt;
&lt;p&gt;For the Game of Fifteen, a permutation of the numbers 1 to 15 is a state of the game if read line by line - like a book.
Some smart people have figured out, that an even parity (is dividable by 2, like 42) is an invariant of a solvable permutation of the Game of Fifteen.
In computer science, an invariant is a condition that is always true, i.e. doesn&#39;t change for a section of code.&lt;/p&gt;
&lt;p&gt;The parity of a permutation is simply the number of inversions.
An inversion happens when an element with a lower index has a higher value than a value at a higher index.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1, 2, 3&lt;/code&gt; has no inversions, all elements are in order&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2, 3, 1&lt;/code&gt; has two inversions, because &lt;code&gt;2&lt;/code&gt; is greater than &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;3&lt;/code&gt; is greater than &lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1, 3, 2&lt;/code&gt; has only one inversion, &lt;code&gt;3&lt;/code&gt; is greater than &lt;code&gt;2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are about 1.3 Trillion (Billion for us people in Central Europe) possible permutations of the numbers 1 to 15.
Only half of them have an even parity.
When we calculate a random permutation for the start of a game, it then makes sense to filter out all the permutations with an odd parity.
Because we do not want to present an unsolvable game.&lt;/p&gt;
&lt;p&gt;Here is a fragment of source code that calculates the parity and checks if it&#39;s even for the examples from above.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; for (let i = 0; i &amp;lt; limit; i += 1) {...} vs&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; range0(limit).forEach((i) =&amp;gt; {...})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; range0&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;limit&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;Array&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;limit&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;keys&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; isEvenPermutation&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;p&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; parity&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; %&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 2&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; ===&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; parity&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;p&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; range0&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt;length&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-d-16&quot;&gt;  .&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;i&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; range0&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt;length&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-d-16&quot;&gt;    .&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;filter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;j&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; i&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span&gt; j&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; p&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; p&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;j&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-d-16&quot;&gt;    .&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt;length&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-d-16&quot;&gt;  .&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;reduce&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;agg&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt; v&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; agg&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span&gt; v&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; true, 0 inversions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;console&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;isEvenPermutation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 2&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 3&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; true, 2 inversions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;console&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;isEvenPermutation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 3&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; false, 1 inversion&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;console&lt;/span&gt;&lt;span class=&quot;z-d-16&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;isEvenPermutation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 3&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 2&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;source-code&quot;&gt;Source code&lt;/h2&gt;
&lt;p&gt;I have created this &lt;a rel=&quot;external&quot; href=&quot;https://gist.github.com/thomasweitzel/3fd3197616f0299a2935972edd68bd4a&quot;&gt;GitHub Gist&lt;/a&gt; with the complete HTML page including the JavaScript source code.
It runs in the browser and uses just &lt;a rel=&quot;external&quot; href=&quot;https://tailwindcss.com&quot;&gt;Tailwind CSS&lt;/a&gt; for styling and plain ES6-style JavaScript.
The source code can be used for all square board sizes, not only 4 by 4.
You have to adjust the boards GUI or dynamically generate it - it&#39;s easy.&lt;/p&gt;
&lt;div class=&quot;hidden bg-primary-300 bg-primary-500 bg-primary-600 bg-primary-800 bg-primary-950 bg-warn-700 text-neutral-950 text-fail-200 border-primary-700 border-warn-600 border-transparent focus:ring-primary-500 focus:ring-offset-neutral-950&quot;&gt;&lt;/div&gt;
&lt;script src=&quot;game-of-fifteen.js&quot; integrity=&quot;sha384-JxrdBI5c+wPKQv/3jeIZHhvFK5u658hFW/SaEFvVQZkE3Obo6boo+t3mu8dXS/ts&quot;&gt;&lt;/script&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Tail recursion with trampoline</title>
        <published>2019-03-16T00:00:00+00:00</published>
        <updated>2019-03-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            Thomas Weitzel
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/cms/tail-recursion-with-trampoline/"/>
        <id>/pages/cms/tail-recursion-with-trampoline/</id>
        
        <content type="html" xml:base="/pages/cms/tail-recursion-with-trampoline/">&lt;p&gt;In case your platform lacks support for proper tail call optimization and you want to do functional programming, you might run into stack space problems.
This can be mitigated, but you have to find a way yourself to make it work.
The specific platform I&#39;m writing about here is &lt;a rel=&quot;external&quot; href=&quot;https://nodejs.org/en/about&quot;&gt;Node.js&lt;/a&gt;.
After a little introduction, I show one well-known solution to the problem.&lt;/p&gt;
&lt;p&gt;In functional programming, recursion is a fundamental concept for handling mutability.
Pure functional languages like Haskell do not allow mutation of variables, so you have to come up with a different solution to the problem.
What you do is called &lt;a rel=&quot;external&quot; href=&quot;https://alvinalexander.com/scala/functional-programming-simplified-book&quot;&gt;update as you copy&lt;/a&gt;.
You create new variables by calculating their values from the values of existing ones.
But you never assign a new value to a variable that already has one.&lt;/p&gt;
&lt;h2 id=&quot;recursion&quot;&gt;Recursion&lt;/h2&gt;
&lt;p&gt;To simplify the reasoning, I will assume that a function does not rely on or modify external state, i.e. values of variables outside of its body.
Math is organized this way: &lt;code&gt;1 + 2 = 3&lt;/code&gt; independently of e.g. the current weather outside.
It&#39;s always true, no matter what.&lt;/p&gt;
&lt;p&gt;In order to write a useful recursive function, you have to make sure that it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;eventually returns with a value (terminates)&lt;/li&gt;
&lt;li&gt;calls itself&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By using these criteria for a recursive function, you can see that it has to either call itself or terminate.
When it terminates, it&#39;s done and you have a value.
Otherwise it will call itself, and you get another stack frame with this call.
Depending on the number of calls this function makes to itself, the used stack space grows and you might eventually run out of stack space.
This is not a good thing!&lt;/p&gt;
&lt;h2 id=&quot;tail-recursion&quot;&gt;Tail recursion&lt;/h2&gt;
&lt;p&gt;What if you do not need the current stack frame anymore, once you make your recursive call?
You could immediately forget it, throw it away, or reuse it, thereby saving stack space.
But you have to make sure that you need no information from this stack frame once you have made your next call.
This can be accomplished by returning the recursive call alone, not adding to it or modifying it once it returns, so the return value can be directly passed on.
Now you have what&#39;s called a tail-recursive function.&lt;/p&gt;
&lt;h2 id=&quot;tail-call-optimization&quot;&gt;Tail call optimization&lt;/h2&gt;
&lt;p&gt;Once you have made sure that your function is tail-recursive, your runtime environment or compiler can optimize the calls in such a way,
that the existing stack frame is reused by the next call.
The stack thereby never grows and you have eliminated the potential for a stack overflow.
But it&#39;s not enough that it can be optimized, it has to be actually done.
That&#39;s where the problem starts.&lt;/p&gt;
&lt;p&gt;Not every runtime environment or compiler performs tail call optimization.
As of this writing two of the platforms that I use do not support tail call optimization out-of-the-box:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://nodejs.org/en&quot;&gt;Node.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Java Virtual Machine (JVM) with &lt;a rel=&quot;external&quot; href=&quot;https://openjdk.java.net/projects/jdk8u&quot;&gt;Java 8&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The JVM does support tail call optimization when used with Kotlin or Scala though.&lt;/p&gt;
&lt;p&gt;Not all is lost: where tail call optimization is not directly supported you can implement a workaround that is straightforward.
It&#39;s effective in saving you from stack overflow errors.
The rest of this article shows you how to implement it.
It&#39;s known under the name &lt;a rel=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Tail_call#Through_trampolining&quot;&gt;trampoline device&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I use the factorial function and implement it in different ways, starting with a loop that mutates values.
Factorial is defined for natural numbers without the 0 (1, 2, 3, ...).
&lt;code&gt;Factorial(n)&lt;/code&gt; calculates the product of all numbers between &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;n&lt;/code&gt;.
It serves as an example that I chose because it&#39;s easy to understand.
I&#39;ll then transform it to a recursive function and show you how to make this function tail-recursive.
Afterwards I introduce the trampoline device and apply it to a function that is the modified tail-recursive function.
Let&#39;s start.&lt;/p&gt;
&lt;h2 id=&quot;loop&quot;&gt;Loop&lt;/h2&gt;
&lt;p&gt;If you&#39;re an &lt;a rel=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Imperative_programming&quot;&gt;imperative programmer&lt;/a&gt;, you would implement the factorial function with a loop.
You start with a variable &lt;code&gt;result&lt;/code&gt; that has the value &lt;code&gt;1&lt;/code&gt; and then have a for-loop that starts at &lt;code&gt;2&lt;/code&gt; and goes up to &lt;code&gt;n&lt;/code&gt;.
In the body of the for-loop you modify &lt;code&gt;result&lt;/code&gt; and set it to a value that was its previous value multiplied by whatever value your loop-variable has.
If you are done with the loop you return the &lt;code&gt;result&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; loopFactorial&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;n&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  let&lt;/span&gt;&lt;span&gt; result&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;let&lt;/span&gt;&lt;span&gt; i&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 2&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;span&gt; i&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;lt;=&lt;/span&gt;&lt;span&gt; n&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;span&gt; i&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +=&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; result&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *=&lt;/span&gt;&lt;span&gt; i&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  return&lt;/span&gt;&lt;span&gt; result&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Something to note here is that your &lt;code&gt;result&lt;/code&gt; variable does not contain the result.
It holds the result only when the execution reaches the return statement.
How do you effectively name a variable that has different meanings during its life cycle?&lt;/p&gt;
&lt;h2 id=&quot;recursive-with-info-left-on-the-stack&quot;&gt;Recursive with info left on the stack&lt;/h2&gt;
&lt;p&gt;How do you slice a carrot?
Although there are many possible ways to do it, one would be to cut off one slice from one end and then repeat the procedure with the rest of the carrot, until there&#39;s nothing left.
You do the same with whatever is left until you reach the terminating condition.
It&#39;s a recursive definition.
And it&#39;s essentially what you do to calculate the factorial in a recursive way.&lt;/p&gt;
&lt;p&gt;To calculate the factorial of &lt;code&gt;n&lt;/code&gt; you multiply &lt;code&gt;n&lt;/code&gt; with the factorial of &lt;code&gt;n - 1&lt;/code&gt;.
You now have to calculate the factorial of &lt;code&gt;n - 1&lt;/code&gt;.
And so on.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; stackFactorial&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;n&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; return&lt;/span&gt;&lt;span&gt; n&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; stackFactorial&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  return&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You need the result of &lt;code&gt;stackFactorial(n - 1)&lt;/code&gt; before you can return from the function call, because you have to multiply it with &lt;code&gt;n&lt;/code&gt; before that can happen.
What &lt;code&gt;n&lt;/code&gt; actually is, is stored in the context of your &lt;strong&gt;current&lt;/strong&gt; stack frame.
The next call will have another stack frame, where &lt;code&gt;n&lt;/code&gt; has another value.
Because you cannot forget the &lt;code&gt;n&lt;/code&gt; of the current call, the current stack frame has to stay around.&lt;/p&gt;
&lt;p&gt;With large &lt;code&gt;n&lt;/code&gt; this can lead to a situation where you get an error and the entire process stops: &lt;code&gt;RangeError: Maximum call stack size exceeded&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;recursive-without-info-left-on-the-stack&quot;&gt;Recursive without info left on the stack&lt;/h2&gt;
&lt;p&gt;What can be done about it?
As a first step, you can eliminate the need for the current stack frame once you made the next call.
But you somehow have to preserve information that&#39;s needed.
The solution is to pass all required information on to the next recursive call.&lt;/p&gt;
&lt;p&gt;An extra parameter is needed to hold this information.
It&#39;s commonly called an aggregator, or &lt;code&gt;agg&lt;/code&gt; for short.
For multiplication you start at &lt;code&gt;1&lt;/code&gt; (identity, &lt;code&gt;n * 1 === n&lt;/code&gt; and &lt;code&gt;1 * n === n&lt;/code&gt;).
Along the way, you multiply the aggregator with the current value of &lt;code&gt;n&lt;/code&gt;, thereby keeping track of how far you&#39;ve come with the product.
The aggregator will look like this over time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 * n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 * n * (n - 1)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 * n * (n - 1) * (n - 2)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you finally hit &lt;code&gt;1&lt;/code&gt; for &lt;code&gt;n&lt;/code&gt;, you simply return the aggregator, because it already contains the correct result.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; tailFactorial&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt; agg&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; return&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; tailFactorial&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; agg&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span&gt; n&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  return&lt;/span&gt;&lt;span&gt; agg&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, the call &lt;code&gt;tailFactorial(...)&lt;/code&gt; is directly returned, no information from the current stack frame is required anymore.&lt;/p&gt;
&lt;h2 id=&quot;alternative-function-signature&quot;&gt;Alternative function signature&lt;/h2&gt;
&lt;p&gt;Even though you invoke &lt;code&gt;tailFactorial(5)&lt;/code&gt;, the definition has two parameters, &lt;code&gt;n&lt;/code&gt; and &lt;code&gt;agg&lt;/code&gt;.
The aggregator has a default value, so you need not provide it.
But since it&#39;s exposed in the function&#39;s interface, someone can call &lt;code&gt;tailFactorial(5, 0)&lt;/code&gt; and get a wrong result.
Because nobody should mess with &lt;code&gt;agg&lt;/code&gt;, it&#39;s better to hide it.&lt;/p&gt;
&lt;p&gt;Wrap it with a function that only allows the parameter &lt;code&gt;n&lt;/code&gt; and make it an inner function of that wrapper function.
Within that wrapper function, pass the call on to the inner function.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Alternative way (not exposing the aggregator)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; tailFactorialAlt&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;n&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  const&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; innerTailFactorial&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt; agg&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; return&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; innerTailFactorial&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; agg&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span&gt; x&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;    return&lt;/span&gt;&lt;span&gt; agg&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  return&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; innerTailFactorial&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I will not use this alternative function signature, because it&#39;s too verbose and not helping me make my point.
Still, I highly recommend that you do not expose parameters in your function interface that are neither needed nor intended for public access.&lt;/p&gt;
&lt;h2 id=&quot;returning-a-function-without-calling-it&quot;&gt;Returning a function without calling it&lt;/h2&gt;
&lt;p&gt;Instead of having the tail-recursive function make the tail call itself, it returns a function (with no arguments) that can call the recursive function with all arguments already in place.
This is called a &lt;a rel=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Thunk#Functional_programming&quot;&gt;thunk&lt;/a&gt;.
You then simply have someone to call that thunk, because the tail call is no longer made by the function.
If it returns another function, you call it again. And again.
Until the final value is returned.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; thunkFactorial&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt; agg&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; &amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; return&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; thunkFactorial&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; agg&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; *&lt;/span&gt;&lt;span&gt; n&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  return&lt;/span&gt;&lt;span&gt; agg&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the function returns either a value or another function without calling it, it is neither recursive nor tail-recursive anymore.
The stack cannot grow and you will not run out of it.
But if your first call does not give you the final result, you end up with just another function and not with the result.&lt;/p&gt;
&lt;h2 id=&quot;device-for-pulling-out-functions-and-calling-them&quot;&gt;Device for pulling out functions and calling them&lt;/h2&gt;
&lt;p&gt;Since you simply pull functions/thunks out of your factorial function until the final value is returned (instead of yet another function), you have to set up a device that&#39;s doing just that.
The device is called a trampoline and it is handed a function that returns either thunks or a final value.
If a call to that function results in another function, it is called for as long as the returned type is a function.
Otherwise it returns the final result.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; trampoline&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt; fn&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;args&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  let&lt;/span&gt;&lt;span&gt; res&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; fn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt;...&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  while&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;res&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; instanceof&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-3&quot;&gt; Function&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; res&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; res&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  return&lt;/span&gt;&lt;span&gt; res&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;putting-it-into-action&quot;&gt;Putting it into action&lt;/h2&gt;
&lt;p&gt;You have two things right now, a function that is returning thunks and another function that can call these thunks for as long as needed, i.e. the final result is returned.
The last thing that&#39;s left to do is to join these two functions together.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-1&quot;&gt; trampolineFactorial&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; trampoline&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;thunkFactorial&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;z-l-5 z-d-4 z-d-i&quot;&gt; Example call&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt;trampolineFactorial&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt;5&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All this is for Node.js and JavaScript.&lt;/p&gt;
&lt;h2 id=&quot;tail-recursive-fibonacci-numbers&quot;&gt;Tail recursive Fibonacci numbers&lt;/h2&gt;
&lt;p&gt;The Fibonacci sequence is defined like this: &lt;code&gt;F(0) = 0&lt;/code&gt; and &lt;code&gt;F(1) = 1&lt;/code&gt;.
For all &lt;code&gt;n &amp;gt; 1&lt;/code&gt; you calculate them with &lt;code&gt;F(n) = F(n - 1) + F(n - 2)&lt;/code&gt;.
This is a recursive definition.&lt;/p&gt;
&lt;p&gt;What is different from the factorial example used above?
You need two independent information, the two previous values of the function.
When you want to solve the problem with tail recursion, instead of one aggregator parameter, you need to pass two aggregator parameters to the function: the two previous values.
As a rule, you need an additional parameter for every additional information that you need in the body of your function.&lt;/p&gt;
&lt;p&gt;Interestingly, &lt;code&gt;n&lt;/code&gt; is used as a counter, going down to &lt;code&gt;1&lt;/code&gt;, but the function works its way up to the higher values.
For each call, the &lt;code&gt;previous&lt;/code&gt; value becomes the &lt;code&gt;current&lt;/code&gt; value from the previous call, while the &lt;code&gt;current&lt;/code&gt; value is set to &lt;code&gt;current + previous&lt;/code&gt;.
Until the function terminates for &lt;code&gt;n === 1&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;giallo z-l-code z-d-code&quot; &gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; F&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt; previous&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-l-8 z-d-11&quot;&gt; current&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; ===&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; return&lt;/span&gt;&lt;span&gt; previous&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; ===&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt; return&lt;/span&gt;&lt;span&gt; current&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-l-9 z-d-17&quot;&gt;  return&lt;/span&gt;&lt;span class=&quot;z-l-7 z-d-18&quot;&gt; F&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; -&lt;/span&gt;&lt;span class=&quot;z-l-1 z-d-11&quot;&gt; 1&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; current&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;,&lt;/span&gt;&lt;span&gt; current&lt;/span&gt;&lt;span class=&quot;z-l-9 z-d-16&quot;&gt; +&lt;/span&gt;&lt;span&gt; previous&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;span class=&quot;z-d-9&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can wrap it in a function with only one parameter for &lt;code&gt;n&lt;/code&gt;, which hides the aggregators &lt;code&gt;current&lt;/code&gt; and &lt;code&gt;next&lt;/code&gt; if you want.
Like in the example above, the original function becomes an inner function of the wrapper function.&lt;/p&gt;
&lt;p&gt;As I recently found out, Fibonacci numbers also have a &lt;a rel=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Fibonacci_number#Matrix_form&quot;&gt;closed-form expression&lt;/a&gt;:&lt;/p&gt;
&lt;div class=&quot;math math-display&quot; role=&quot;math&quot;&gt;&amp;#92;[F&amp;#95;{n}={&amp;#92;cfrac {1}{&amp;#92;sqrt {5}}}&amp;#92;left({&amp;#92;cfrac {1+{&amp;#92;sqrt {5}}}{2}}&amp;#92;right)^{n}-{&amp;#92;cfrac {1}{&amp;#92;sqrt {5}}}&amp;#92;left({&amp;#92;cfrac {1-{&amp;#92;sqrt {5}}}{2}}&amp;#92;right)^{n}&amp;#92;]&lt;/div&gt;
&lt;p&gt;As an aside: we were &lt;a rel=&quot;external&quot; href=&quot;https://www.mountaingoatsoftware.com/agile/planning-poker&quot;&gt;estimating story points&lt;/a&gt; during a sprint planing.
Story points roughly follow the Fibonacci numbers, so it&#39;s natural that the topic pops up from time to time.
Someone was asking if there was a closed-form expression for Fibonacci numbers, so we searched for an answer. And voilà ...&lt;/p&gt;
&lt;p&gt;Using the trampoline device together with the modified tail-recursive Fibonacci function is left as an exercise for the reader.
Hint: you have to modify the tail-recursive Fibonacci function to return a function/thunk instead of returning the result of a recursive function call.&lt;/p&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;I use the trampoline device because Node.js does not support tail call optimization (TCO).
At one point I thought that Node.js would eventually support TCO through its use of Google&#39;s V8 engine. Google was actively working on supporting TCO in V8.
But it turned out that Google is &lt;a rel=&quot;external&quot; href=&quot;https://www.chromestatus.com/feature/5516876633341952&quot;&gt;no longer pursuing&lt;/a&gt; this path.
Since tail-recursive functions are particularly useful in functional programming, it&#39;s extremely frustrating to see so little progress.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Privacy Policy</title>
        <published>2019-03-11T00:00:00+00:00</published>
        <updated>2019-03-11T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="/pages/privacy/"/>
        <id>/pages/privacy/</id>
        
        <content type="html" xml:base="/pages/privacy/">&lt;h2 id=&quot;general-notes&quot;&gt;General notes&lt;/h2&gt;
&lt;p&gt;Protecting your personal data matters. This website is a &lt;strong&gt;static information site&lt;/strong&gt; and can be used &lt;strong&gt;without providing personal data&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I do &lt;strong&gt;not&lt;/strong&gt; actively collect personal data: there are no contact forms, no tracking scripts, no marketing services, and no comments or user accounts. The only exception is basic aggregate traffic statistics (hit/visitor counts) via Cloudflare&#39;s dashboard — see &lt;a href=&quot;/pages/privacy/#hosting&quot;&gt;Hosting&lt;/a&gt; below.&lt;/p&gt;
&lt;h2 id=&quot;responsible-controller&quot;&gt;Responsible controller&lt;/h2&gt;
&lt;p&gt;The responsible controller for data processing on this website is Paul Richards, based in London, UK, reachable via the &lt;a href=&quot;/pages/contact/&quot;&gt;contact page&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;hosting&quot;&gt;Hosting&lt;/h2&gt;
&lt;p&gt;This website is served from a self-managed origin server that I administer directly. Traffic to the site is routed
through &lt;strong&gt;Cloudflare&lt;/strong&gt;, which sits in front of the origin as a content delivery network and reverse proxy, handling
TLS encryption, caching, and protection against malicious traffic (e.g. DDoS attacks).&lt;/p&gt;
&lt;p&gt;The origin server does &lt;strong&gt;not&lt;/strong&gt; keep access logs — this is a static site with nothing for per-request logging to
usefully record. Cloudflare&#39;s edge network does process technical connection data (e.g. IP address, requested page,
timestamp, browser/OS information) as an inherent part of routing and protecting the traffic it proxies. I use
Cloudflare&#39;s aggregate dashboard analytics for a basic count of hits and visitors; this is derived from the traffic
Cloudflare&#39;s edge already handles, shows aggregate figures rather than tracking identifiable individuals, and does
not involve any script, cookie, or code added to the site itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Legal basis:&lt;/strong&gt; Art. 6(1)(f) UK GDPR &lt;br /&gt;
&lt;strong&gt;Legitimate interest:&lt;/strong&gt; secure and stable operation of the website, and basic traffic visibility&lt;/p&gt;
&lt;p&gt;Cloudflare processes this connection data as an independent controller/processor for its network services; details
are available in &lt;a rel=&quot;external&quot; href=&quot;https://www.cloudflare.com/privacypolicy/&quot;&gt;Cloudflare&#39;s privacy policy&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;cookies&quot;&gt;Cookies and browser data storage&lt;/h2&gt;
&lt;p&gt;This website itself does not set cookies. Cloudflare, which sits in front of the site as described in the
&lt;a href=&quot;/pages/privacy/#hosting&quot;&gt;Hosting&lt;/a&gt; section above, may set its own strictly-necessary security cookies (e.g. for bot/DDoS
protection) independently of this site — see &lt;a rel=&quot;external&quot; href=&quot;https://www.cloudflare.com/privacypolicy/&quot;&gt;Cloudflare&#39;s privacy policy&lt;/a&gt;
for details.&lt;/p&gt;
&lt;p&gt;Browser storage is used in one case: after an &lt;strong&gt;active click on the theme toggle&lt;/strong&gt;, to store the display setting
(&lt;code&gt;light&lt;/code&gt; or &lt;code&gt;dark&lt;/code&gt;) for this session in the browser (&lt;code&gt;sessionStorage&lt;/code&gt;). This is exempt from consent requirements
under the UK Privacy and Electronic Communications Regulations (PECR), as it is strictly necessary to provide the
display preference you requested.&lt;/p&gt;
&lt;p&gt;The data storage takes place exclusively locally in the browser. Entries can be deleted there at any time.&lt;/p&gt;
&lt;h2 id=&quot;external-content-and-services&quot;&gt;External content and services&lt;/h2&gt;
&lt;p&gt;This website does &lt;strong&gt;not&lt;/strong&gt; embed external content (e.g., YouTube, Google Fonts, tracking scripts).
All resources (scripts, images, etc.) are served locally.&lt;/p&gt;
&lt;h2 id=&quot;ssl-tls-encryption&quot;&gt;SSL/TLS encryption&lt;/h2&gt;
&lt;p&gt;This website uses SSL/TLS encryption. Data transmitted during a page request cannot be read by third parties.&lt;/p&gt;
&lt;h2 id=&quot;rights-of-data-subjects&quot;&gt;Rights of data subjects&lt;/h2&gt;
&lt;p&gt;You have the right to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;access to stored personal data&lt;/li&gt;
&lt;li&gt;correction or deletion&lt;/li&gt;
&lt;li&gt;data portability where applicable&lt;/li&gt;
&lt;li&gt;restriction of processing&lt;/li&gt;
&lt;li&gt;objection to processing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These rights apply to any personal data processed as described above. Note that the origin server keeps no access
logs, and the traffic data Cloudflare processes at its edge is aggregate rather than data I hold or can act on for
individual requests — for data Cloudflare processes directly, see &lt;a rel=&quot;external&quot; href=&quot;https://www.cloudflare.com/privacypolicy/&quot;&gt;Cloudflare&#39;s privacy policy&lt;/a&gt;
for how to exercise these rights with them.&lt;/p&gt;
&lt;p&gt;Requests relating to data I hold can be sent via the &lt;a href=&quot;/pages/contact/&quot;&gt;contact page&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;supervisory-authority-and-right-to-file-a-complaint&quot;&gt;Supervisory authority and right to file a complaint&lt;/h2&gt;
&lt;p&gt;You have the right to file a complaint with a data protection supervisory authority. In the UK, this is the
&lt;a rel=&quot;external&quot; href=&quot;https://ico.org.uk/&quot;&gt;Information Commissioner&#39;s Office (ICO)&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
</feed>
