<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2025-09-26T07:05:35+00:00</updated><id>/feed.xml</id><title type="html">Demian’s blog</title><author><name>Demian Ferreiro</name></author><entry><title type="html">LLMs are not even good wordcels</title><link href="/blog/chatgpt-sucks-at-pangrams" rel="alternate" type="text/html" title="LLMs are not even good wordcels" /><published>2024-06-08T00:00:00+00:00</published><updated>2024-06-08T00:00:00+00:00</updated><id>/blog/chatgpt-sucks-at-pangrams</id><content type="html" xml:base="/blog/chatgpt-sucks-at-pangrams"><![CDATA[<p>A chat with friends recently reminded me about pangrams, and what a cute little language curiosity they are.<span sidenote=""> I also remembered that i never got a <a href="https://en.wikipedia.org/wiki/Pangram#Self-enumerating_pangrams">self-enumerating pangram</a> generator to work. I should give that another try!</span> I thought it would be fun play with ChatGPT and see if it could generate some good ones, expecting it to do quite well on this task. After all, <abbr title="Large Language Models">LLMs</abbr> should be excellent wordcels, right?<span sidenote=""> That is, is there’s one thing they should be very good at, that is verbal intelligence. Yeah, i know <a href="https://roonscape.ai/p/a-song-of-shapes-and-words">this meme</a> of “shape rotators vs. wordcels” can be a bit cringy, but i honestly find these terms ironically endearing.</span> Well, it doesn’t seem so.</p>

<p>Just so we’re on the same page: pangrams are phrases that use every letter of the alphabet. Even if you didn’t know that definition, chances are you already knew of one such phrase: “the quick brown fox jumps over the lazy dog”, which is typically used to showcase typographic fonts.</p>

<p>So, how did a state-of-the-art LLM like ChatGPT-4o fare with pangrams? Let’s see:</p>

<blockquote class="long">
  <p>Demian: hola, sabés lo que es un pangrama?</p>

  <p>ChatGPT: ¡Hola! Sí, un pangrama es una frase o texto que utiliza todas las letras del alfabeto de un idioma al menos una vez. Los pangramas se usan a menudo en pruebas de tipografía, diseño gráfico y en la enseñanza de la mecanografía.</p>

  <p>Un ejemplo clásico en español es: “El veloz murciélago hindú comía feliz cardillo y kiwi.”</p>

  <details>
    <summary>Translation (not really important; the chats are explained below)</summary>
    <p>Demian: hello, do you know what a pangram is?</p>

    <p>ChatGPT: Hello! Yes, a pangram is a phrase or text that uses all the letters of the alphabet of a language at least once. Pangrams are often used in typography tests, graphic design, and in teaching typing.</p>

    <p>A classic example in Spanish is: “El veloz murciélago hindú comía feliz cardillo y kiwi.”</p>
  </details>
</blockquote>

<p>I went with Spanish because, why not? It’s nice when software understands your native language.<span sidenote=""> In fact, maybe even programming languages should be designed to let people program in their mother tongue. Learning how to program when you’re not fluent in English, or when your alphabet is not based on Latin, is more difficult and frustrating than it needs to be. A recent <a href="https://corecursive.com/hedy-with-felienne-hermans/">CoRecursive episode</a> touches on this idea, and i found the guest’s arguments to be very compelling. She created <a href="https://www.hedycode.com/">Hedy</a>, a language that allows students to learn programming using their native tongue. The breadth of languages it supports, from Spanish to Korean, is awe-inspiring :)</span> And, in any case, if it were true that LLMs have an internal model of the world, the output language shouldn’t make much difference.</p>

<p>So, ChatGPT starts with a good definition of what a pangram is, but then it botches the example badly. It doesn’t include <em>many</em> letters: B, J, Ñ, P, Q, S, T, and X.</p>

<p>This is because the sentence it chose is only half of a well-known Spanish pangram that also includes all diacritics:</p>

<blockquote>
  <p>El veloz murciélago hindú comía feliz cardillo y kiwi. La cigüeña tocaba el saxofón detrás del palenque de paja.</p>
</blockquote>

<p>At the moment i only noticed the missing Ñ, which is of course a glaring omission to a Spanish speaker, as it’s such a characteristic letter. It’s even part of the language name!</p>

<p>So i tell ChatGPT about that:</p>

<blockquote class="long">
  <p>Demian: no le falta una letra del español a ese pangrama?</p>

  <p>ChatGPT: Tienes razón, el ejemplo que di no incluye la letra “ñ”. Un pangrama en español que sí utiliza todas las letras del alfabeto, incluida la “ñ”, es:</p>

  <p>“Jovencillo emponzoñado de whisky: ¡qué figurota exhibe!”</p>

  <p>Este pangrama incluye todas las letras del alfabeto español, incluyendo la “ñ”.</p>

  <details>
    <summary>Translation</summary>
    <p>Demian: isn’t that pangram missing a Spanish letter?</p>

    <p>ChatGPT: You’re right, the example I gave does not include the letter “ñ”. A pangram in Spanish that does use all the letters of the alphabet, including “ñ”, is:</p>

    <p>“Jovencillo emponzoñado de whisky: ¡qué figurota exhibe!”</p>

    <p>This pangram includes all the letters of the Spanish alphabet, including “ñ”.</p>
  </details>
</blockquote>

<p>It identifies that the phrase is indeed missing an Ñ,<span sidenote=""> Even though it’s also missing many other letters. But, to be fair, programmers routinely forget about non-ASCII characters too. So ChatGPT might even be emulating that frequent mistake, honoring a tradition learned from an English-centric corpus.</span> and it manages to quote a correct well-known pangram. Yay!</p>

<p>Then i ask it to come up with a novel pangram:</p>

<blockquote class="long">
  <p>Demian: bien. podrías armar un pangrama que no exista en la internet? no hace falta que sea muy breve</p>

  <p>ChatGPT: ¡Claro! Aquí tienes un pangrama en español que incluye todas las letras del alfabeto, incluyendo la “ñ”:</p>

  <p>“El pingüino ñato y jovial, experto en boxeo, lanzó su eficaz jaque mate a la rápida bruja del volcán.”</p>

  <p>Espero que te guste este pangrama original.</p>

  <details>
    <summary>Translation</summary>
    <p>Demian: good. could you put together a pangram that does not exist on the internet? it doesn’t have to be concise.</p>

    <p>ChatGPT: Sure! Here is a pangram in Spanish that includes all the letters of the alphabet, including the “ñ”:</p>

    <p>“El pingüino ñato y jovial, experto en boxeo, lanzó su eficaz jaque mate a la rápida bruja del volcán.”</p>

    <p>I hope you like this original pangram.</p>
  </details>
</blockquote>

<p>Ok, not the most natural-sounding phrase.<span sidenote=""> Roughly, “The small-nosed and jovial penguin, an expert in boxing, launched his effective checkmate to the fast witch of the volcano”</span> But fair enough, i didn’t say anything about the phrase having to make much sense.</p>

<p>However, a keen eye will notice that this is not a pangram: it’s missing the letters H, K and W.</p>

<p>So then i ask ChatGPT what i thought would be a trivial task: to identify which 3 letters are missing from that phrase:</p>

<blockquote class="long">
  <p>Demian: le faltan 3 letras del castellano a ese intento de pangrama. qué letras le faltan?</p>

  <p>ChatGPT: Mis disculpas por el error. Revisaré el pangrama para identificar las letras que faltan.</p>

  <p>El pangrama proporcionado es:
“El pingüino ñato y jovial, experto en boxeo, lanzó su eficaz jaque mate a la rápida bruja del volcán.”</p>

  <p>Voy a revisar si incluye todas las letras del alfabeto español:</p>
  <ul>
    <li>a, b, c, d, e, f, g, h, i, j, k, l, m, n, ñ, o, p, q, r, s, t, u, v, w, x, y, z</li>
  </ul>

  <p>Al revisar, noto que faltan las letras <strong>h</strong>, <strong>k</strong> y <strong>s</strong>. Intentaré nuevamente crear un pangrama completo:</p>

  <hr />

  <p>“El pingüino ñato y jovial husmeaba feliz, cuando la bruja rápida y excéntrica le dio un box con whisky.”</p>

  <hr />

  <p>Este pangrama incluye todas las letras del alfabeto español.</p>

  <details>
    <summary>Translation</summary>
    <p>Demian: that attempt at a pangram is missing 3 Spanish letters. what letters are missing?</p>

    <p>ChatGPT: My apologies for the error. I will review the pangram to identify the missing letters.</p>

    <p>The provided pangram is:
“El pingüino ñato y jovial, experto en boxeo, lanzó su eficaz jaque mate a la rápida bruja del volcán.”</p>

    <p>I’m going to check if it includes all the letters of the Spanish alphabet:</p>
    <ul>
      <li>a, b, c, d, e, f, g, h, i, j, k, l, m, n, ñ, o, p, q, r, s, t, u, v, w, x, z</li>
    </ul>

    <p>Upon checking, I notice that the letters <strong>h</strong>, <strong>k</strong>, and <strong>s</strong> are missing. I’ll try to create a complete pangram again:</p>

    <hr />

    <p>“El pingüino ñato y jovial husmeaba feliz, cuando la bruja rápida y excéntrica le dio un box con whisky.”</p>

    <hr />

    <p>This pangram includes all the letters of the Spanish alphabet.</p>
  </details>
</blockquote>

<p>In typical ChatGPT fashion, it spouts a lengthy answer that makes it seem like it’s reasoning about which letters are missing from the previous pangram attempt.</p>

<p>But it’s wrong. It says the phrase was missing the H, K and S letters, when it clearly had an S (in the word “su”), and it failed to spot the missing W.</p>

<p>Then it gives another incorrect pangram example while still confidently proclaiming it includes all Spanish letters.</p>

<p>Honestly, i was surprised by how poorly ChatGPT did on this prompt. Checking whether a phrase is a pangram is a trivial task that can be done with <a href="https://www.youtube.com/watch?v=PA_zfdH9pX0">a simple Bash one-liner</a>. And of all “AI” models, i thought an LLM would be especially good at identifying missing letters on words.</p>

<p>But i think this is faulty reasoning on my part. I’m using a wrong mental model of what an LLM is. More on this at the end.</p>

<p>Losing patience, i tell it about the missing Q on its last example, but then give it a more interesting task: to generate a phrase that contains all letters except S:</p>

<blockquote class="long">
  <p>Demian: no incluye la Q. olvidate de eso. podés generar una frase que tenga todas las letras del castellano excepto la S?</p>

  <p>ChatGPT: Por supuesto, aquí tienes un pangrama en español que incluye todas las letras del alfabeto excepto la “s”:</p>

  <p>“El pingüino ñato y jovial brinca feliz, comiendo kiwi y exquisito budín de limón.”</p>

  <p>Este pangrama utiliza todas las letras del alfabeto español, excepto la “s”.</p>

  <details>
    <summary>Translation</summary>
    <p>Demian: it doesn’t include the Q. forget about that. can you generate a phrase that has all the letters of Spanish except S?</p>

    <p>ChatGPT: Of course, here is a pangram in Spanish that includes all the letters of the alphabet except “s”:</p>

    <p>“El pingüino ñato y jovial brinca feliz, comiendo kiwi y exquisito budín de limón.”</p>

    <p>This pangram uses all the letters of the Spanish alphabet, except “s”.</p>
  </details>
</blockquote>

<p>An obvious failure once again: the word “exquisito” has an S. I didn’t even check if the phrase has all the other letters.</p>

<blockquote class="long">
  <p>Demian: la palabra “exquisito” tiene una S</p>

  <p>ChatGPT: Tienes razón, me equivoqué. Aquí va otro intento:</p>

  <p>“El rápido murciélago ñoño volaba feliz comiendo kiwi, jabalí y pizza con budín.”</p>

  <p>Este pangrama debería incluir todas las letras del alfabeto español excepto la “s”.</p>

  <details>
    <summary>Translation</summary>
    <p>Demian: the word “exquisito” has an S</p>

    <p>ChatGPT: You’re right, I made a mistake. Here’s another attempt:</p>

    <p>“El rápido murciélago ñoño volaba feliz comiendo kiwi, jabalí y pizza con budín.”</p>

    <p>This pangram should include all the letters of the Spanish alphabet except “s”.</p>
  </details>
</blockquote>

<p>At last, it manages to produce a phrase without an S as instructed. But once again it fails to include every other letter, while still confidently claiming to have done so.</p>

<p>I call it quits after this last attempt.</p>

<h2 id="some-closing-thoughts">Some closing thoughts</h2>

<p>I don’t know what to think about this little exercise in futility to be honest.</p>

<p>One thing that is clear to me is that we’re vastly overestimating the “intelligence” of LLMs, if it even makes sense to use that word in this context. We’re being fooled by their apparent capacity to produce human-sounding text.</p>

<p>This semblance of intelligence quickly falls apart when you start prodding the LLMs and they start “making mistakes” that no person would make. A person can easily check if a phrase contains all the letters in the alphabet, even if it’s a phrase they’ve never seen before. Hell, even if the phrase is in a language they don’t know. It’s an easy task.</p>

<p>But an LLM will make trivial errors while at the same time giving the impression of competently having done what you asked for.</p>

<p>One thing LLMs seem incapable of doing is to say “i don’t know”.</p>

<h3 id="on-our-mental-model-of-llms">On our mental model of LLMs</h3>

<p>I think we’re being too generous to LLMs when we talk about what they do. That’s why i used quotes around “making mistakes” or “intelligence”. We’re using a wrong mental model of what LLMs are. An LLM is not making mistakes, or even trying to answer a question. It’s only “trying” to predict text, word after word, based on all the text it’s been trained on.</p>

<p>And once we start thinking about an LLM as a text predictor, things start to make more sense:</p>

<ul>
  <li>It can claim that some phrase is a pangram because its training data included similar phrases on texts that talked about pangrams. It’s not like it learned the general rule of what a pangram is or how to check if a phrase is a pangram or not.</li>
  <li>It will generally give an answer after we ask a question because that’s what its training corpus has. Questions are generally followed by answers.</li>
  <li>It won’t say “i don’t know” because its training data doesn’t have that many examples of questions being answered like that. People write about questions they have successfully answered. And when someone asks a question on the internet, they generally receive answers, not “i don’t know”s. And that’s the text the LLM has been trained on, so it makes sense for it to predict an answer-looking text after a question, even if the answer is incorrect.</li>
</ul>

<p>Maybe we’re attributing a false cause for this capacity of LLMs to produce text.</p>

<p>We humans have a rich internal mental model of the world. And we use that model to —among many other things— speak somewhat coherently about the world. So, in some way, the text we produce is <em>caused</em> by our internal world model.</p>

<p>But when it comes to LLMs, we observe that they can produce text that is very similar to what we humans produce, and then we conclude that they must also have a rich internal model of the world in order to be able to produce that text. But maybe we’re jumping to this conclusion too early. Maybe we’re anthropomorphizing these things. There could be other mechanisms by which LLMs are able to produce that text.</p>

<p>If we see a photocopier machine producing beautiful art, we don’t jump to the conclusion that the photocopier has somewhat understood what makes great art and became an artist. No, we conclude that someone has put a beautiful image on its scanner. But this is because we understand how a photocopier works internally. But with LLMs we have the problem of them being quite black-boxy, or at least much more so than a photocopier.</p>

<h3 id="on-what-the-future-may-hold">On what the future may hold</h3>

<p>This one is tricky. And i find myself torn between an optimistic and a pessimistic view.</p>

<p>On the one hand, i find it quite relieving to see that even the latest and most powerful “AI” models are still so far off from anything that we could call general intelligence with a straight face.</p>

<p>I don’t think we’re anywhere close to <abbr title="Artificial general intelligence">AGI</abbr>, and i’m glad for that. I don’t know if i’m an “AI doomer” or not, but i generally agree with the statement of <a href="https://www.safe.ai/work/statement-on-ai-risk">AGI posing an existential risk</a> to humanity. I guess i could try to articulate why i think creating something more intelligent (and therefore more powerful) than ourselves seems like a spectacularly stupid idea, and i don’t understand why anyone would want to do that instead of trying to improve humanity in some less naive and risky way,<span sidenote=""> Like, IDK, trying to augment <em>human</em> intelligence, or our capacity for coordination, or for empathy.</span> but this post is too long already.</p>

<p>Suffice to say, any anxiety i may hold for AI killing us all gets subdued by evidence suggesting that we’re still a long way off that. Even though this is contrary to what some AI-hype-bros may say.<span sidenote=""> It’s unsurprising that many of the people who are trying to sell us the “AI revolution” today were also trying to sell us blockchain and NFTs as revolutions not too long ago.</span></p>

<p>On the other hand, however, i do worry about the AI-hype-bros winning, and these dumb inscrutable black boxes known as LLMs being deployed for actually important stuff.</p>

<p>And this is already happening to some extent. Google already provides “AI-based” results that regularly <a href="https://pluralistic.net/2024/05/15/they-trust-me-dumb-fucks/">make the same kind of basic mistake</a> illustrated on this blog post: confidently asserting something generated by a text predictor that’s been trained on a huge corpus of Reddit posts.<span sidenote=""> Nothing against Reddit posts. I regularly add “site:reddit.com” to my Google searches when looking for stuff, especially opinions and reviews. But i also have the common sense to tell apart a genuine comment from a troll or a shitpost. These nuances seem completely opaque to an LLM, understandably so.</span> Mistakes that generally don’t pass the sniff test of a well-informed person, let alone of an expert on a given field.</p>

<p>I can only assume that unscrupulous job recruiters are already using similar models to filter out job applications. So these models may already be leaving people out of jobs. And we’re trusting their overconfident token-prediction-based “judgment”.</p>

<p>This has real-world consequences; it’s not just wrong pangrams.</p>

<p>And we keep pouring more and more money and resources into this.</p>

<p>At this point, my biggest hope for the problem of the misuse of LLMs (or generative models in general) is for this “AI” movement to collapse under its own weight. And maybe after this hype cycle passes, the memories that this current “AI” moment leaves us will be so bitter that we’ll choose to dismantle all its deleterious uses.</p>

<p>In the meantime, let’s remember to touch grass and connect with other people.</p>]]></content><author><name>Demian Ferreiro</name></author><category term="random" /><category term="AI" /><category term="LLMs" /><summary type="html"><![CDATA[A chat with friends recently reminded me about pangrams, and what a cute little language curiosity they are. I also remembered that i never got a self-enumerating pangram generator to work. I should give that another try! I thought it would be fun play with ChatGPT and see if it could generate some good ones, expecting it to do quite well on this task. After all, LLMs should be excellent wordcels, right? That is, is there’s one thing they should be very good at, that is verbal intelligence. Yeah, i know this meme of “shape rotators vs. wordcels” can be a bit cringy, but i honestly find these terms ironically endearing. Well, it doesn’t seem so.]]></summary></entry><entry><title type="html">Sum of factorials tweet “proof”</title><link href="/blog/factorial-digits" rel="alternate" type="text/html" title="Sum of factorials tweet “proof”" /><published>2024-04-19T00:00:00+00:00</published><updated>2024-04-19T00:00:00+00:00</updated><id>/blog/factorial-digits</id><content type="html" xml:base="/blog/factorial-digits"><![CDATA[<!-- TODO: Extract this into a reusable math.html snippet, or better yet: pre-render LaTeX. -->
<script type="text/javascript" async="" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>

<p>Many moons ago, Vsauce <a href="https://twitter.com/tweetsauce/status/965259567322943490">tweeted</a>:</p>

<blockquote>
  <p>1 = 1!<br />
2 = 2!<br />
145 = 1! + 4! + 5!<br />
40,585 = 4! + 0! + 5! + 8! + 5!</p>

  <p>These are the only four numbers with this property.</p>

  <p class="source">— Vsauce (@tweetsauce) <a href="https://twitter.com/tweetsauce/status/965259567322943490">February 18, 2018</a></p>
</blockquote>

<p>Searching for these numbers on <abbr title="The On-Line Encyclopedia of Integer Sequences">OEIS</abbr>, we can find them on the sequence <a href="https://oeis.org/A014080">A014080</a> and learn that they are called <strong>factorions</strong>. Defined as numbers equal to the sum of the <a href="https://en.wikipedia.org/wiki/Factorial">factorials</a> of their digits, there are indeed only four of them (in base 10).</p>

<p>Let’s try to “prove” this by <a href="/blog/programmer-vs-mathematician">once again</a> writing a simple Ruby program that checks all possible numbers, and hopefully confirms that these are the only four factorions.</p>

<p>But <em>which</em> possible numbers? All of them? Do we just leave the computer checking bigger and bigger numbers until we get bored and declare that there must be no more factorions if we haven’t found a new one after an arbitrary limit? We couldn’t call <em>that</em> a proof.</p>

<p>We can try to get an intuition for how these sums behave by comparing the numbers made of only 9s (i.e., 9, 99, 999, and so on) to the sum of the factorials of those 9s (i.e., \(9!\), \(9!{+}9!\), \(9!{+}9!{+}9!\), and so on).</p>

<p><span marginnote="">Note that \(9!\) is \(362\,880\).</span></p>

<p><span marginnote="">And maybe the string formatting here also warrants a comment. In Ruby, “multiplying” a string and number —that is, sending the <code class="language-plaintext highlighter-rouge">*</code> message with a number argument to a string—, like in <code class="language-plaintext highlighter-rouge">'9' * n</code>, returns a string with <code class="language-plaintext highlighter-rouge">n</code> copies of the original. Similarly, multiplying an array with a number, as in <code class="language-plaintext highlighter-rouge">['9!'] * n</code>, returns an array with the elements of the original repeated <code class="language-plaintext highlighter-rouge">n</code> times. And curiously, multiplying an array <em>with a string</em>, as in the second <code class="language-plaintext highlighter-rouge">*</code> of <code class="language-plaintext highlighter-rouge">['9!'] * n * ' + '</code>, is the same as <a href="https://ruby-doc.org/3.3.0/Array.html#method-i-2A">joining the elements</a> of the array with that string. A somewhat cryptic choice, yes, but since we’re dealing with factorials here, multiplying stuff together seemed appropriate.</span></p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">1</span><span class="p">.</span><span class="nf">upto</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span>
  <span class="n">gt</span> <span class="o">=</span> <span class="p">(</span><span class="s1">'9'</span> <span class="o">*</span> <span class="n">n</span><span class="p">).</span><span class="nf">to_i</span> <span class="o">&gt;</span> <span class="n">n</span> <span class="o">*</span> <span class="mi">362_880</span>
  <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="s1">'9'</span> <span class="o">*</span> <span class="n">n</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">gt</span> <span class="p">?</span> <span class="s1">'&gt;'</span> <span class="p">:</span> <span class="s1">'≤'</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="p">[</span><span class="s1">'9!'</span><span class="p">]</span> <span class="o">*</span> <span class="n">n</span> <span class="o">*</span> <span class="s1">' + '</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Which prints:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>9 ≤ 9!
99 ≤ 9! + 9!
999 ≤ 9! + 9! + 9!
9999 ≤ 9! + 9! + 9! + 9!
99999 ≤ 9! + 9! + 9! + 9! + 9!
999999 ≤ 9! + 9! + 9! + 9! + 9! + 9!
9999999 &gt; 9! + 9! + 9! + 9! + 9! + 9! + 9!
99999999 &gt; 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9!
999999999 &gt; 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9!
9999999999 &gt; 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9!
</code></pre></div></div>

<p>So, from \(9\,999\,999\) onwards, the numbers made of only 9s become bigger than the <strong>sum of the factorials of their digits</strong>, or <strong>SFD</strong> for short. And they seem to remain that way, at least up to 10 digits.</p>

<p>A more rigorous way of seeing this is to consider any positive number of \(n\) digits. The number itself must be greater or equal to \(10^{n-1}\) (otherwise it wouldn’t have \(n\) digits), but its SFD can be at most \(n \cdot 9!\) (if it’s all 9s). In other words, as we add more digits, numbers grow exponentially, while their SFDs only grow linearly. This means that <strong>there must be a finite number of factorions</strong>, as the exponential growth of the numbers with each new digit will outpace the linear growth of the SFD at some point.</p>

<p>And for 7 digits we can already see that \(9\,999\,999\) is greater than \(7 \cdot 9! = 2\,540\,160\). In fact it’s even greater than 8 or 9 times \(9!\), the maximum SFDs for numbers of 8 and 9 digits. So at that point numbers are definitely greater than their SFDs, which means that if we “only” check all numbers up to \(9\,999\,999\), we can be sure to find all possible factorions.</p>

<p>Then let the brute-forcing begin!</p>

<p><span marginnote="">Note the very succinct and cute factorial definition: <code class="language-plaintext highlighter-rouge">(1..n).reduce(1, :*)</code>. We’re taking advantage of <code class="language-plaintext highlighter-rouge">reduce()</code> accepting both a block or a symbol for the reducer function. Given the <code class="language-plaintext highlighter-rouge">:*</code> symbol, <code class="language-plaintext highlighter-rouge">reduce()</code> combines the numbers <code class="language-plaintext highlighter-rouge">1..n</code> using their <code class="language-plaintext highlighter-rouge">*</code> method. In other words: it multiplies them together.</span></p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Store the factorials from 0 to 9 to avoid recalculating them each time.</span>
<span class="n">factorials</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="mi">9</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span><span class="o">|</span><span class="n">n</span><span class="o">|</span> <span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="n">n</span><span class="p">).</span><span class="nf">reduce</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="p">:</span><span class="o">*</span><span class="p">)</span> <span class="p">}</span>

<span class="mi">1</span><span class="p">.</span><span class="nf">upto</span><span class="p">(</span><span class="mi">9_999_999</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span>
  <span class="n">sfd</span> <span class="o">=</span> <span class="n">n</span><span class="p">.</span><span class="nf">digits</span><span class="p">.</span><span class="nf">map</span><span class="p">{</span><span class="o">|</span><span class="n">d</span><span class="o">|</span> <span class="n">factorials</span><span class="p">[</span><span class="n">d</span><span class="p">]</span> <span class="p">}.</span><span class="nf">sum</span>
  <span class="k">if</span> <span class="n">n</span> <span class="o">==</span> <span class="n">sfd</span>
    <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">n</span><span class="si">}</span><span class="s2"> = </span><span class="si">#{</span><span class="n">n</span><span class="p">.</span><span class="nf">digits</span><span class="p">.</span><span class="nf">reverse</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span><span class="o">|</span><span class="n">d</span><span class="o">|</span> <span class="s2">"</span><span class="si">#{</span><span class="n">d</span><span class="si">}</span><span class="s2">!"</span><span class="si">}</span><span class="s2">.join(' + ')}"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Running this snippet, which takes around 6 seconds of number-crunching on my computer, outputs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 = 1!
2 = 2!
145 = 1! + 4! + 5!
40585 = 4! + 0! + 5! + 8! + 5!
</code></pre></div></div>

<p>Which matches all the equations on the original Vsauce tweet. We’ve found the four factorions!</p>

<p>Q.E.D.</p>

<div style="height:20rem"></div>
<p>…</p>
<div style="height:20rem"></div>

<p>Hmmm… 6 seconds to run that snippet. Isn’t that… kind of slow?</p>

<p>I mean, this has nothing to do with the proof, but modern computers can do many <em>billions</em> of instructions per second. And we’re doing a linear search over only 10 million numbers. Even if some of the operations on that loop could be somewhat expensive, like calling the <code class="language-plaintext highlighter-rouge">digits</code> method to get the digits of a number, it <em>feels</em> like a computer should be able to run this much faster.</p>

<p>But that will be a topic for another post!</p>]]></content><author><name>Demian Ferreiro</name></author><category term="maths" /><category term="ruby" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Programming vs. mathematical curiosity</title><link href="/blog/programmer-vs-mathematician" rel="alternate" type="text/html" title="Programming vs. mathematical curiosity" /><published>2024-04-12T00:00:00+00:00</published><updated>2024-04-12T00:00:00+00:00</updated><id>/blog/programmer-vs-mathematician</id><content type="html" xml:base="/blog/programmer-vs-mathematician"><![CDATA[<p>Recently i came across a puzzling property of numbers that got me to reflect on what “tickles” my mind as a programmer. And how programmers and mathematicians, even though they share much in common —a mix of playfulness and rigour, and a knack for logic problem-solving—, might be motivated by very different things.<span sidenote=""> Of course, this is all painting with very broad strokes. Every person is different.</span></p>

<p>Consider this simple question: what would happen if we pick a number, arrange its digits in descending and ascending order to get two different numbers, and then subtract those two?</p>

<p>For example, let’s pick the number 1988. Arranging its digits in descending and ascending order we get 9881 and 1889, and subtracting those two we get 7992. Riveting, yes. But what would happen if we repeat this process now starting from 7992, and then keep going?</p>

<p>Please take a moment and consider what would happen. Not necessarily to check it with pen and paper, but what does your intuition say?</p>

<p>Well, let’s see. Starting from 1988 the sequence would go:</p>

<p><span marginnote="">Sidenote: if we had started from 2023 —the year i started writing and procrastinating this blog post— the sequence would look almost the same. The first step would be 3220 − 0223 = 2997, and after that it would continue the same way. This is not such a cosmic coincidence as it seems; the numbers on these sequences tend to repeat a lot.</span></p>

<!-- Hacky way of aligning the numbers in the sequence. Libre Baskerville doesn't have tabular numbers -->
<style>
  .kap-seq { display:table }
  .kap-seq > * { display: table-row }
  .kap-seq > * > * { display: table-cell; white-space: preserve }
</style>

<blockquote class="kap-seq">
<div>1998<span> → </span>9881<span> − </span>1889<span> = </span>7992</div>
<div>7992<span> → </span>9972<span> − </span>2799<span> = </span>7173</div>
<div>7173<span> → </span>7731<span> − </span>1377<span> = </span>6354</div>
<div>6354<span> → </span>6543<span> − </span>3456<span> = </span>3087</div>
<div>3087<span> → </span>8730<span> − </span>0378<span> = </span>8352</div>
<div>8352<span> → </span>8532<span> − </span>2358<span> = </span>6174</div>
<div>6174<span> → </span>7641<span> − </span>1467<span> = </span>6174</div>
<div>6174<span> → </span>7641<span> − </span>1467<span> = </span>6174</div>
<div>…</div>
</blockquote>

<p>Once we hit 6174 the sequence starts repeating, as the result of applying this “biggest digit-arrangement minus smallest digit-arrangement” operation to 6174, is 6174 itself.<span sidenote=""> Or in maths parlance: 6174 is a <a href="https://en.wikipedia.org/wiki/Fixed_point_(mathematics)">fixed point</a> of this operation.</span></p>

<p>Now here’s the kicker: as long as the starting number is not a single repeated digit, <strong>we can start from any 4-digit number and the sequence will always reach 6174</strong>.</p>

<p>Of course, this is not a new discovery. 6174 is known as <a href="https://en.wikipedia.org/wiki/6174_(number)">Kaprekar’s constant</a>, in honor of the mathematician who found this curious property.</p>

<p>I don’t know about you, but this surprised me when i first heard about it. All numbers converging to a single one? Instead of maybe converging to different numbers, or getting into loops? Who would’ve expected that!?</p>

<p>But i also didn’t have the mathematical tools —or curiosity really— to dig into the maths of this to understand it more deeply. Luckily, there aren’t that many 4-digit numbers, and computers are pretty fast, or so i’ve been told. So instead of mathematically proving this the “correct” way, let’s have some fun and write a simple Ruby program to check this property for every 4-digit number.<span sidenote=""> We could use any language really. I like using Ruby for this kind of explorations because it gets out of the way and allows expressing things quite freely.</span></p>

<h3 id="a-simple-subproblem">A simple subproblem</h3>

<p>Let’s do some bottom-up development and start with the simplest bit: checking if a number consists of a single repeating digit. These numbers are called <a href="https://en.wikipedia.org/wiki/Repdigit">repdigits</a> in recreational mathematics.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">repdigit?</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">=</span> <span class="n">n</span><span class="p">.</span><span class="nf">digits</span><span class="p">.</span><span class="nf">uniq</span><span class="p">.</span><span class="nf">size</span> <span class="o">==</span> <span class="mi">1</span>
</code></pre></div></div>

<p>There’s quite a bit going on on this line if you’re not used to Ruby, so let’s break it apart. The <code class="language-plaintext highlighter-rouge">n.digits</code> method gives us an array with the number’s base-10 digits. Then we filter out repeated digits using <code class="language-plaintext highlighter-rouge">uniq</code>. So we’re basically saying: a number is a repdigit if it has only one distinct digit. The code reads almost as a condensed version of its English explanation.</p>

<p>By the way, if you’re wondering about this <code class="language-plaintext highlighter-rouge">def name(args) = expr</code> weirdness, that is Ruby’s <a href="https://bugs.ruby-lang.org/issues/16746">“endless” method</a> syntax, which debuted on version 3.0. I’m still on the fence about it to be honest. On the one hand, it seems like the kind of unnecessary syntax sugar that tends to bloat languages and complicates answering basic questions like “how do i define a method?”. But at the same time, it can be useful to distinguish the cases where something can be expressed succinctly in a single expression, and the cases where multiple statements are needed.</p>

<p>Oh and yes, method names can end with <code class="language-plaintext highlighter-rouge">?</code>, which is actually the convention for boolean predicates in Ruby.</p>

<p>Anyway, back to our program! This <code class="language-plaintext highlighter-rouge">repdigit?</code> definition is correct and works for numbers of any length. But we only care about 4-digit numbers. And Kaprekar’s property also works for smaller numbers if we extend them with leading zeroes. For example, 777 is a repdigit, but if we consider it a 4-digit number and write it as 0777 then we can start the sequence with 7770 - 0777 = 6993 and we’d reach Kaprekar’s constant in 3 more steps.</p>

<p>So we only care about repdigits of 4 digits like 1111, 2222, and so on. In other words, multiples of 1111:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">repdigit?</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">=</span> <span class="n">n</span> <span class="o">%</span> <span class="mi">1111</span> <span class="o">==</span> <span class="mi">0</span>
</code></pre></div></div>

<h3 id="kaprekars-routine">Kaprekar’s routine</h3>

<p>Jumping up a bit in abstraction level, the logic of repeatedly rearranging the digits of a number into ascending and descending order, and subtracting those two until the result starts to repeat —aka Kaprekar’s routine— can be encoded as:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">kap</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
  <span class="n">d</span> <span class="o">=</span> <span class="n">descending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">-</span> <span class="n">ascending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
  <span class="n">n</span> <span class="o">==</span> <span class="n">d</span> <span class="p">?</span> <span class="n">n</span> <span class="p">:</span> <span class="n">kap</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We take the difference of the two rearrangements of the number’s digits (which we haven’t defined yet). If that difference is equal to the input number, then we have found where the sequence starts repeating and we return that number. And if it’s a different number, we recur and do the same thing again this time with the difference.</p>

<p>Note that this recursive function only terminates if the operation reaches the same number at some point. But we <em>know</em> that to be true; that’s the whole point of writing this program! We just want to check this programmatically. It doesn’t make sense to defend against an infinite recursion that we know won’t happen. Even more: <a href="https://en.wikipedia.org/wiki/Kaprekar%27s_routine#Kaprekar's_constants_in_base_10">it is known</a> that for 4-digit numbers this operation actually converges in 7 or fewer steps, so if we preferred iteration over recursion we could’ve written:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">kap</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
  <span class="mi">7</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span>
    <span class="n">n</span> <span class="o">=</span> <span class="n">descending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">-</span> <span class="n">ascending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
  <span class="k">end</span>
  <span class="n">n</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Both versions will work fine for our purposes. If for some reason Kaprekar’s property doesn’t hold for some number, the iterative version will give us a wrong result, and the recursive version will hang forever or overflow the stack.<span sidenote=""> Which of the two? That’s <a href="https://nithinbekal.com/posts/ruby-tco/">an interesting question</a>!</span> Either way, we would know that we have screwed up our “proof” somewhere. <em>It’s fine.</em></p>

<h3 id="rearranging-digits">Rearranging digits</h3>

<p>Rearranging the digits of a number to be in ascending order is straightforward now that we know about the <code class="language-plaintext highlighter-rouge">digits</code> method:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">ascending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">=</span> <span class="n">n</span><span class="p">.</span><span class="nf">digits</span><span class="p">.</span><span class="nf">sort</span><span class="p">.</span><span class="nf">join</span><span class="p">.</span><span class="nf">to_i</span>
</code></pre></div></div>

<p>We take the digits of the number, sort them, then <code class="language-plaintext highlighter-rouge">join</code> them into a string, and then convert that string to an integer with <code class="language-plaintext highlighter-rouge">to_i</code>. Maybe a bit too many unnecessary allocations for some tastes, but for a small proof program like this, it’s totally fine.</p>

<p>We can quickly test this on Ruby’s interactive console, <code class="language-plaintext highlighter-rouge">irb</code>:</p>

<p><span marginnote=""><abbr title="Read-eval-print loop">REPLs</abbr> are great for this kind of exploratory programming. Instead of the more “classic” approach of writing a program on a source file, running it, tweaking it, running it again, etc, a REPL allow for a much tighter, almost immediate, feedback loop. Once you have your little bits of program written and tested on the console, you can copy them onto a program file.</span></p>

<div class="language-irb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">$ irb
</span><span class="gp">irb(main):001&gt;</span><span class="w"> </span><span class="k">def</span> <span class="nf">ascending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">=</span> <span class="n">n</span><span class="p">.</span><span class="nf">digits</span><span class="p">.</span><span class="nf">sort</span><span class="p">.</span><span class="nf">join</span><span class="p">.</span><span class="nf">to_i</span>
<span class="p">=&gt;</span> <span class="ss">:ascending_digits</span>
<span class="gp">irb(main):002&gt;</span><span class="w"> </span><span class="n">ascending_digits</span><span class="p">(</span><span class="mi">3713</span><span class="p">)</span>
<span class="p">=&gt;</span> <span class="mi">1337</span>
</code></pre></div></div>

<p>Now, for rearranging the digits in <em>descending</em> order, a naive solution could be to just add a <code class="language-plaintext highlighter-rouge">reverse</code> call after sorting:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">descending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">=</span> <span class="n">n</span><span class="p">.</span><span class="nf">digits</span><span class="p">.</span><span class="nf">sort</span><span class="p">.</span><span class="nf">reverse</span><span class="p">.</span><span class="nf">join</span><span class="p">.</span><span class="nf">to_i</span>
</code></pre></div></div>

<p>But this logic hides a sneaky bug. Can you spot it?</p>

<p>We can test <code class="language-plaintext highlighter-rouge">descending_digits</code> on the console, which seems to work fine and even makes <code class="language-plaintext highlighter-rouge">kap(n)</code> return the Kaprekar constant for the example number we started with:</p>

<div class="language-irb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">irb(main):007&gt;</span><span class="w"> </span><span class="n">descending_digits</span><span class="p">(</span><span class="mi">1988</span><span class="p">)</span>
<span class="p">=&gt;</span> <span class="mi">9881</span>
<span class="gp">irb(main):008&gt;</span><span class="w"> </span><span class="n">kap</span><span class="p">(</span><span class="mi">1988</span><span class="p">)</span>
<span class="p">=&gt;</span> <span class="mi">6174</span>
</code></pre></div></div>

<p>But when called with the first number of 4 digits, 1000, we get:</p>

<div class="language-irb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">irb(main):009&gt;</span><span class="w"> </span><span class="n">kap</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span>
<span class="p">=&gt;</span> <span class="mi">0</span>
</code></pre></div></div>

<p>Which is not right. 1000 is not a repdigit. It should not converge to 0.</p>

<p>What’s happening here is that, on the first iteration, <code class="language-plaintext highlighter-rouge">ascending_digits(1000) == 1</code> and <code class="language-plaintext highlighter-rouge">descending_digits(1000) == 1000</code>, which are both correct. Their difference is 999 though, and when we try to get its digits’ rearrangements on the second iteration we get 999 for both the ascending and descending variants, which of course then causes the result of the subtraction to be 0 and then repeat.</p>

<p>We need to make <code class="language-plaintext highlighter-rouge">descending_digits</code> consider its inputs as 4-digit numbers, so that 999 is seen as 0999, and then its descending order digit rearrangement would be 9990 instead of 999. Here’s one way of doing this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">descending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">=</span> <span class="n">n</span><span class="p">.</span><span class="nf">digits</span><span class="p">.</span><span class="nf">sort</span><span class="p">.</span><span class="nf">reverse</span><span class="p">.</span><span class="nf">join</span><span class="p">.</span><span class="nf">ljust</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="s1">'0'</span><span class="p">).</span><span class="nf">to_i</span>
</code></pre></div></div>

<p>After sorting the digits and joining them into a string, we’re using <code class="language-plaintext highlighter-rouge">ljust(4, '0')</code> to pad the string with zeroes to the right so it is 4 characters long.<span sidenote=""> A bit hacky, yes, but it gets the job done. At least it’s quite succinct, and readable enough. If you can think of a better alternative, please let me know!</span></p>

<p>With that hotfix in, <code class="language-plaintext highlighter-rouge">kap(n)</code> now seems to work as intended:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">011</span><span class="o">&gt;</span> <span class="n">kap</span><span class="p">(</span><span class="mi">1988</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="mi">6174</span>
<span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">012</span><span class="o">&gt;</span> <span class="n">kap</span><span class="p">(</span><span class="mi">1000</span><span class="p">)</span>
<span class="o">=&gt;</span> <span class="mi">6174</span>
</code></pre></div></div>

<h3 id="the-final-proof">The final “proof”</h3>

<p>Now at the top of this bottom-up process, we can define the main logic of the program:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">1</span><span class="p">.</span><span class="nf">upto</span><span class="p">(</span><span class="mi">9999</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span>
  <span class="n">repdigit?</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="ow">or</span> <span class="n">kap</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">==</span> <span class="mi">6174</span> <span class="ow">or</span>
    <span class="nb">fail</span> <span class="s2">"</span><span class="si">#{</span><span class="n">n</span><span class="si">}</span><span class="s2"> is not a repdigit nor does it converge to 6174"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Again, notice how Ruby code can read as a condensed version of its English translation: for every number from 1 up to 9999, the number is either a repdigit, or its Kaprekar sequence converges to 6174. If neither of these is true, the “proof” fails.</p>

<p>You may be wondering why i used the wordy <code class="language-plaintext highlighter-rouge">or</code> boolean operator instead of the more common symbolic <code class="language-plaintext highlighter-rouge">||</code> alternative. Well, it’s a stylistic choice really. First, if i had used <code class="language-plaintext highlighter-rouge">||</code>, the <code class="language-plaintext highlighter-rouge">fail</code> method call would’ve needed parentheses around its argument, which i prefer to avoid since i think of <code class="language-plaintext highlighter-rouge">fail</code> as control flow and i like to visually distinguish control flow methods from other “normal” method calls, like <code class="language-plaintext highlighter-rouge">kap(n)</code> or <code class="language-plaintext highlighter-rouge">repdigit?(n)</code> in this case. And second, using <code class="language-plaintext highlighter-rouge">and</code> and <code class="language-plaintext highlighter-rouge">or</code> as control flow operators —as alternatives to <code class="language-plaintext highlighter-rouge">if</code> and <code class="language-plaintext highlighter-rouge">unless</code> respectively— is <a href="https://avdi.codes/using-and-and-or-in-ruby/">a brilliant idea</a> and can help expressing things in the “natural” way we conceive them.<span sidenote=""> Although, i’ll admit this might be a form of Stockholm Syndrome; a rationalization of Ruby’s <a href="https://en.wikipedia.org/wiki/There%27s_more_than_one_way_to_do_it">multiple ways</a> of doing the same thing. Generally i prefer languages having one obvious way of doing things. So i’ll think about my choice here as a rare exception to my usual structuredness :)</span></p>

<p>Stitching all these snippets together, the complete “proof” program is:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">kap</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
  <span class="n">d</span> <span class="o">=</span> <span class="n">descending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">-</span> <span class="n">ascending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
  <span class="n">n</span> <span class="o">==</span> <span class="n">d</span> <span class="p">?</span> <span class="n">n</span> <span class="p">:</span> <span class="n">kap</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">ascending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">=</span> <span class="n">n</span><span class="p">.</span><span class="nf">digits</span><span class="p">.</span><span class="nf">sort</span><span class="p">.</span><span class="nf">join</span><span class="p">.</span><span class="nf">to_i</span>
<span class="k">def</span> <span class="nf">descending_digits</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">=</span> <span class="n">n</span><span class="p">.</span><span class="nf">digits</span><span class="p">.</span><span class="nf">sort</span><span class="p">.</span><span class="nf">reverse</span><span class="p">.</span><span class="nf">join</span><span class="p">.</span><span class="nf">ljust</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="s1">'0'</span><span class="p">).</span><span class="nf">to_i</span>
<span class="k">def</span> <span class="nf">repdigit?</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">=</span> <span class="n">n</span> <span class="o">%</span> <span class="mi">1111</span> <span class="o">==</span> <span class="mi">0</span>

<span class="mi">1</span><span class="p">.</span><span class="nf">upto</span><span class="p">(</span><span class="mi">9999</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">n</span><span class="o">|</span>
  <span class="n">repdigit?</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="ow">or</span> <span class="n">kap</span><span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="o">==</span> <span class="mi">6174</span> <span class="ow">or</span>
    <span class="nb">fail</span> <span class="s2">"</span><span class="si">#{</span><span class="n">n</span><span class="si">}</span><span class="s2"> is not a repdigit nor does it converge to 6174"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And running it confirms that, indeed, Kaprekar’s routine converges to 6174 for all 4-digit numbers as long as the number is not a single repeated digit.</p>

<h3 id="wait-wasnt-this-about-maths-or-something">Wait, wasn’t this about maths or something?</h3>

<p>Well, yes. And no. The story starts as a maths puzzle, but it’s really about the journey after that.</p>

<p>In my case at least, when i learned about Kaprekar’s result, my mind immediately jumped from “that cannot be!” to “i <em>must</em> write a program to check this for all numbers!”. And you can see where my curiosity wandered while writing that program:</p>

<ul>
  <li>to different ways of expressing the same idea and trying to find one i like the best</li>
  <li>to recursion vs iteration</li>
  <li>to programming languages’ questionable syntactic choices</li>
  <li>to programming languages’ philosophies</li>
  <li>to writing software top-down vs bottom-up</li>
</ul>

<p>Of course, these are all things that interest me, and so naturally my mind went there. Once i got the program written and running, the little speck of mathematical curiosity i initially had was gone.</p>

<p>I suspect a more maths-oriented person would go through this very differently. Maybe they’d try to find an elegant analytical proof, without the aid of a dumb number-crunching machine brute-forcing its way through. Or they’d try to generalize the problem to different number of digits, or different number bases. I don’t really know.</p>

<p>But i appreciate having played with this. It seems i’m not that into maths as i once thought. And that i love programming. Who would have thought?</p>

<p>And how about you? What was on <em>your</em> mind while your eyes read this?<span sidenote=""> Assuming they did, in which case: thank you very much for coming along.</span></p>]]></content><author><name>Demian Ferreiro</name></author><category term="maths" /><category term="ruby" /><summary type="html"><![CDATA[Recently i came across a puzzling property of numbers that got me to reflect on what “tickles” my mind as a programmer. And how programmers and mathematicians, even though they share much in common —a mix of playfulness and rigour, and a knack for logic problem-solving—, might be motivated by very different things. Of course, this is all painting with very broad strokes. Every person is different.]]></summary></entry><entry><title type="html">Bash variable substitutions &amp;amp; less tricks</title><link href="/blog/bash-var-substitution" rel="alternate" type="text/html" title="Bash variable substitutions &amp;amp; less tricks" /><published>2023-04-05T00:00:00+00:00</published><updated>2023-04-05T00:00:00+00:00</updated><id>/blog/bash-var-substitution</id><content type="html" xml:base="/blog/bash-var-substitution"><![CDATA[<p>It’s always nice to learn a new trick that may ever so slightly improve the way we use our tools. Today i learned two such little tricks regarding the command-line.</p>

<p>The first one is that, in Bash, we can do a <code class="language-plaintext highlighter-rouge">sed</code>-like substitution on the contents of a variable using the syntax <code class="language-plaintext highlighter-rouge">${var/pattern/substitution}</code>.</p>

<p>As a toy example, consider converting a Reddit URL to the old (and arguably better) version of the site:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nv">url</span><span class="o">=</span><span class="s2">"https://reddit.com/r/commandline/"</span>
<span class="gp">$</span><span class="w"> </span><span class="nb">echo</span> <span class="k">${</span><span class="nv">url</span><span class="p">/reddit/old.reddit</span><span class="k">}</span>
<span class="go">https://old.reddit.com/r/commandline/
</span><span class="gp">$</span><span class="w"> </span>xdg-open <span class="k">${</span><span class="nv">url</span><span class="p">/reddit/old.reddit</span><span class="k">}</span>
</code></pre></div></div>

<p>This seems like a more convenient way of doing simple text replacements than having to pipe things to a <code class="language-plaintext highlighter-rouge">sed</code> subprocess.</p>

<p>And there’s a lot more to Bash’s cryptic but powerful <code class="language-plaintext highlighter-rouge">${}</code> syntax: case conversion, substring slicing, you name it. The manpage has a thorough description under the “Parameter Expansion” heading:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>man bash | less <span class="nt">-p</span> <span class="s2">"Parameter Expansion"</span>
</code></pre></div></div>

<p>Which, by the way, demonstrates the second of today’s tricks: we can use the <code class="language-plaintext highlighter-rouge">-p</code> (or <code class="language-plaintext highlighter-rouge">--pattern</code>) argument in <code class="language-plaintext highlighter-rouge">less</code> to scroll directly to the first occurrence of a given pattern.</p>

<p>I doubt that i’ll be using this one often, as it seems more convenient to search for the pattern interactively using <code class="language-plaintext highlighter-rouge">/</code> when already inside of <code class="language-plaintext highlighter-rouge">less</code> instead. But i can imagine using it as a neat “documentation pointer” to share a <code class="language-plaintext highlighter-rouge">less</code> command with others, just like the above snippet was handed to me :)</p>

<p>Thanks to my friend —and seasoned Linux geek— <a href="https://twitter.com/racter/">@racter</a> for showing me these two tricks. I found them quite neat and maybe worth sharing. Or at least, worth using as an excuse to finally start writing a blog, which, here it is! So, hello world! 👋</p>]]></content><author><name>Demian Ferreiro</name></author><category term="TIL" /><category term="bash" /><summary type="html"><![CDATA[It’s always nice to learn a new trick that may ever so slightly improve the way we use our tools. Today i learned two such little tricks regarding the command-line.]]></summary></entry></feed>