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.

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

Demian: hola, sabés lo que es un pangrama?

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.

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

## Translation (not really important; the chats are explained below)

Demian: hello, do you know what a pangram is?

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.

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

I went with Spanish because, why not? It’s nice when software understands your native language. 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 CoRecursive episode touches on this idea, and i found the guest’s arguments to be very compelling. She created Hedy, 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 :) 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.

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

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

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.

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!

So i tell ChatGPT about that:

Demian: no le falta una letra del español a ese pangrama?

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:

“Jovencillo emponzoñado de whisky: ¡qué figurota exhibe!”

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

## Translation

Demian: isn’t that pangram missing a Spanish letter?

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:

“Jovencillo emponzoñado de whisky: ¡qué figurota exhibe!”

This pangram includes all the letters of the Spanish alphabet, including “ñ”.

It identifies that the phrase is indeed missing an Ñ, 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. and it manages to quote a correct well-known pangram. Yay!

Then i ask it to come up with a novel pangram:

Demian: bien. podrías armar un pangrama que no exista en la internet? no hace falta que sea muy breve

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

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

Espero que te guste este pangrama original.

## Translation

Demian: good. could you put together a pangram that does not exist on the internet? it doesn’t have to be concise.

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

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

I hope you like this original pangram.

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

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

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

Demian: le faltan 3 letras del castellano a ese intento de pangrama. qué letras le faltan?

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

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.”

Voy a revisar si incluye todas las letras del alfabeto español:

- 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
Al revisar, noto que faltan las letras

h,kys. Intentaré nuevamente crear un pangrama completo:

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

Este pangrama incluye todas las letras del alfabeto español.

## Translation

Demian: that attempt at a pangram is missing 3 Spanish letters. what letters are missing?

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

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.”

I’m going to check if it includes all the letters of the Spanish alphabet:

- 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
Upon checking, I notice that the letters

h,k, andsare missing. I’ll try to create a complete pangram again:

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

This pangram includes all the letters of the Spanish alphabet.

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.

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.

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

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 simple Bash one-liner. And of all “AI” models, i thought an LLM would be especially good at identifying missing letters on words.

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.

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:

Demian: no incluye la Q. olvidate de eso. podés generar una frase que tenga todas las letras del castellano excepto la S?

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

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

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

## Translation

Demian: it doesn’t include the Q. forget about that. can you generate a phrase that has all the letters of Spanish except S?

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

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

This pangram uses all the letters of the Spanish alphabet, except “s”.

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

Demian: la palabra “exquisito” tiene una S

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

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

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

## Translation

Demian: the word “exquisito” has an S

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

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

This pangram should include all the letters of the Spanish alphabet except “s”.

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.

I call it quits after this last attempt.

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

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.

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.

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

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

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.

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

- 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.
- 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.
- 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.

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

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 *caused* by our internal world model.

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.

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.

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

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.

I don’t think we’re anywhere close to AGI, 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 AGI posing an existential risk 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, Like, IDK, trying to augment *human* intelligence, or our capacity for coordination, or for empathy. but this post is too long already.

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. 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.

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.

And this is already happening to some extent. Google already provides “AI-based” results that regularly make the same kind of basic mistake illustrated on this blog post: confidently asserting something generated by a text predictor that’s been trained on a huge corpus of Reddit posts. 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. Mistakes that generally don’t pass the sniff test of a well-informed person, let alone of an expert on a given field.

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”.

This has real-world consequences; it’s not just wrong pangrams.

And we keep pouring more and more money and resources into this.

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.

In the meantime, let’s remember to touch grass and connect with other people.

]]>Many moons ago, Vsauce tweeted:

1 = 1!

2 = 2!

145 = 1! + 4! + 5!

40,585 = 4! + 0! + 5! + 8! + 5!These are the only four numbers with this property.

— Vsauce (@tweetsauce) February 18, 2018

Searching for these numbers on OEIS, we can find them on the sequence A014080 and learn that they are called **factorions**. Defined as numbers equal to the sum of the factorials of their digits, there are indeed only four of them (in base 10).

Let’s try to “prove” this by once again writing a simple Ruby program that checks all possible numbers, and hopefully confirms that these are the only four factorions.

But *which* 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 *that* a proof.

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).

Note that \(9!\) is \(362\,880\).

And maybe the string formatting here also warrants a comment. In Ruby, “multiplying” a string and number —that is, sending the `*`

message with a number argument to a string—, like in `'9' * n`

, returns a string with `n`

copies of the original. Similarly, multiplying an array with a number, as in `['9!'] * n`

, returns an array with the elements of the original repeated `n`

times. And curiously, multiplying an array *with a string*, as in the second `*`

of `['9!'] * n * ' + '`

, is the same as joining the elements of the array with that string. A somewhat cryptic choice, yes, but since we’re dealing with factorials here, multiplying stuff together seemed appropriate.

```
1.upto(10) do |n|
gt = ('9' * n).to_i > n * 362_880
puts "#{'9' * n} #{gt ? '>' : '≤'} #{['9!'] * n * ' + '}"
end
```

Which prints:

```
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 > 9! + 9! + 9! + 9! + 9! + 9! + 9!
99999999 > 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9!
999999999 > 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9!
9999999999 > 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9! + 9!
```

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

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 **there must be a finite number of factorions**, as the exponential growth of the numbers with each new digit will outpace the linear growth of the SFD at some point.

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.

Then let the brute-forcing begin!

Note the very succinct and cute factorial definition: `(1..n).reduce(1, :*)`

. We’re taking advantage of `reduce()`

accepting both a block or a symbol for the reducer function. Given the `:*`

symbol, `reduce()`

combines the numbers `1..n`

using their `*`

method. In other words: it multiplies them together.

```
# Store the factorials from 0 to 9 to avoid recalculating them each time.
factorials = (0..9).map {|n| (1..n).reduce(1, :*) }
1.upto(9_999_999) do |n|
sfd = n.digits.map{|d| factorials[d] }.sum
if n == sfd
puts "#{n} = #{n.digits.reverse.map {|d| "#{d}!"}.join(' + ')}"
end
end
```

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

```
1 = 1!
2 = 2!
145 = 1! + 4! + 5!
40585 = 4! + 0! + 5! + 8! + 5!
```

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

Q.E.D.

…

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

I mean, this has nothing to do with the proof, but modern computers can do many *billions* 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 `digits`

method to get the digits of a number, it *feels* like a computer should be able to run this much faster.

But that will be a topic for another post!

]]>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?

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?

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

Well, let’s see. Starting from 1988 the sequence would go:

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.

1998 → 9881 − 1889 = 79927992 → 9972 − 2799 = 71737173 → 7731 − 1377 = 63546354 → 6543 − 3456 = 30873087 → 8730 − 0378 = 83528352 → 8532 − 2358 = 61746174 → 7641 − 1467 = 61746174 → 7641 − 1467 = 6174…

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. Or in maths parlance: 6174 is a fixed point of this operation.

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

Of course, this is not a new discovery. 6174 is known as Kaprekar’s constant, in honor of the mathematician who found this curious property.

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!?

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. 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.

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 repdigits in recreational mathematics.

```
def repdigit?(n) = n.digits.uniq.size == 1
```

There’s quite a bit going on on this line if you’re not used to Ruby, so let’s break it apart. The `n.digits`

method gives us an array with the number’s base-10 digits. Then we filter out repeated digits using `uniq`

. 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.

By the way, if you’re wondering about this `def name(args) = expr`

weirdness, that is Ruby’s “endless” method 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.

Oh and yes, method names can end with `?`

, which is actually the convention for boolean predicates in Ruby.

Anyway, back to our program! This `repdigit?`

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.

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

```
def repdigit?(n) = n % 1111 == 0
```

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:

```
def kap(n)
d = descending_digits(n) - ascending_digits(n)
n == d ? n : kap(d)
end
```

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.

Note that this recursive function only terminates if the operation reaches the same number at some point. But we *know* 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: it is known 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:

```
def kap(n)
7.times do
n = descending_digits(n) - ascending_digits(n)
end
n
end
```

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. Which of the two? That’s an interesting question! Either way, we would know that we have screwed up our “proof” somewhere. *It’s fine.*

Rearranging the digits of a number to be in ascending order is straightforward now that we know about the `digits`

method:

```
def ascending_digits(n) = n.digits.sort.join.to_i
```

We take the digits of the number, sort them, then `join`

them into a string, and then convert that string to an integer with `to_i`

. Maybe a bit too many unnecessary allocations for some tastes, but for a small proof program like this, it’s totally fine.

We can quickly test this on Ruby’s interactive console, `irb`

:

REPLs 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.

```
$ irb
irb(main):001> def ascending_digits(n) = n.digits.sort.join.to_i
=> :ascending_digits
irb(main):002> ascending_digits(3713)
=> 1337
```

Now, for rearranging the digits in *descending* order, a naive solution could be to just add a `reverse`

call after sorting:

```
def descending_digits(n) = n.digits.sort.reverse.join.to_i
```

But this logic hides a sneaky bug. Can you spot it?

We can test `descending_digits`

on the console, which seems to work fine and even makes `kap(n)`

return the Kaprekar constant for the example number we started with:

```
irb(main):007> descending_digits(1988)
=> 9881
irb(main):008> kap(1988)
=> 6174
```

But when called with the first number of 4 digits, 1000, we get:

```
irb(main):009> kap(1000)
=> 0
```

Which is not right. 1000 is not a repdigit. It should not converge to 0.

What’s happening here is that, on the first iteration, `ascending_digits(1000) == 1`

and `descending_digits(1000) == 1000`

, 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.

We need to make `descending_digits`

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:

```
def descending_digits(n) = n.digits.sort.reverse.join.ljust(4, '0').to_i
```

After sorting the digits and joining them into a string, we’re using `ljust(4, '0')`

to pad the string with zeroes to the right so it is 4 characters long. 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!

With that hotfix in, `kap(n)`

now seems to work as intended:

```
irb(main):011> kap(1988)
=> 6174
irb(main):012> kap(1000)
=> 6174
```

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

```
1.upto(9999) do |n|
repdigit?(n) or kap(n) == 6174 or
fail "#{n} is not a repdigit nor does it converge to 6174"
end
```

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.

You may be wondering why i used the wordy `or`

boolean operator instead of the more common symbolic `||`

alternative. Well, it’s a stylistic choice really. First, if i had used `||`

, the `fail`

method call would’ve needed parentheses around its argument, which i prefer to avoid since i think of `fail`

as control flow and i like to visually distinguish control flow methods from other “normal” method calls, like `kap(n)`

or `repdigit?(n)`

in this case. And second, using `and`

and `or`

as control flow operators —as alternatives to `if`

and `unless`

respectively— is a brilliant idea and can help expressing things in the “natural” way we conceive them. Although, i’ll admit this might be a form of Stockholm Syndrome; a rationalization of Ruby’s multiple ways 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 :)

Stitching all these snippets together, the complete “proof” program is:

```
def kap(n)
d = descending_digits(n) - ascending_digits(n)
n == d ? n : kap(d)
end
def ascending_digits(n) = n.digits.sort.join.to_i
def descending_digits(n) = n.digits.sort.reverse.join.ljust(4, '0').to_i
def repdigit?(n) = n % 1111 == 0
1.upto(9999) do |n|
repdigit?(n) or kap(n) == 6174 or
fail "#{n} is not a repdigit nor does it converge to 6174"
end
```

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.

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

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

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

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.

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.

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?

And how about you? What was on *your* mind while your eyes read this? Assuming they did, in which case: thank you very much for coming along.

The first one is that, in Bash, we can do a `sed`

-like substitution on the contents of a variable using the syntax `${var/pattern/substitution}`

.

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

```
$ url="https://reddit.com/r/commandline/"
$ echo ${url/reddit/old.reddit}
https://old.reddit.com/r/commandline/
$ xdg-open ${url/reddit/old.reddit}
```

This seems like a more convenient way of doing simple text replacements than having to pipe things to a `sed`

subprocess.

And there’s a lot more to Bash’s cryptic but powerful `${}`

syntax: case conversion, substring slicing, you name it. The manpage has a thorough description under the “Parameter Expansion” heading:

```
man bash | less -p "Parameter Expansion"
```

Which, by the way, demonstrates the second of today’s tricks: we can use the `-p`

(or `--pattern`

) argument in `less`

to scroll directly to the first occurrence of a given pattern.

I doubt that i’ll be using this one often, as it seems more convenient to search for the pattern interactively using `/`

when already inside of `less`

instead. But i can imagine using it as a neat “documentation pointer” to share a `less`

command with others, just like the above snippet was handed to me :)

Thanks to my friend —and seasoned Linux geek— @racter 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! 👋

]]>