Enter RPN

Project Euler Problem 1
Login

Project Euler Problem 1

The Most Artificial of Problems

I find Project Euler Problem #1 useful for evaluating the capability of old, primitive programming languages precisely because you can solve it in a few lines of code in anything modern, defined here as equaling or exceeding the capability of 1991’s Perl 4:

#!/usr/bin/perl
$r0 = 0;
foreach $x (1..999) {
    $r0 += $x if $x % 3 == 0
              || $x % 5 == 0;
}
print $r0, "\n";

Here, $r0 is an accumulator meant to evoke the HP Voyager series’ first general-purpose register, which we will come back to shortly. The $x iteration variable roughly parallels the use of the RPN stack’s x register.

If we’re willing to perpetrate code golfing atrocities, we can achieve the same end in a single line of code without changing the algorithm, either to cheat by reframing the problem or to hard-code results known only because we ran it once and found the answer:

$ perl -E 'for(1..999){$r+=$_ unless $_%3&&$_%5}say $r'

Setting such dubious amusements aside, we have more substantive questions to ask and answer here. How much longer does it get if we try to achieve the same end on a 1980s era programmable HP RPN calculator? What interesting conclusions can we come to by comparing the results?

The Solutions

HP-12C

The first thing I tried is the least-capable qualifying calculator I own,1 with this result:

        CL𝑥             # zero x without bumping stack
        STO 0           # zero R0 = initial accumulated total
        999             # starting iteration, down to 0
        STO 1           # save counter to R1
07      3               # x % 3 == 0?
        ÷
        g FRAC
        g x=0?          # zero remainder?
        g GTO 20        # yes; accumulate R1
        RCL 1           # retrieve iteration count
        5               # x % 5 == 0?
        ÷
        g FRAC
        g x=0?          # zero remainder?
        g GTO 20        # yes; accumulate R1
        RCL 1           # no; decrement iteration
        g GTO 22
20      RCL 1
        STO+ 0
22      1               # common decrement-by-1 step
        -
        g x=0?
        g GTO 28
        STO 1
        g GTO 07        # more work to do; next iteration!
28      RCL 0           # we're done; copy R0 into x for display to user
        g GTO 00        # then end

For clarity, I show line numbers only when they’re used as jump targets, which brings us our first observation: the 12C lacks labels. You simply start the program at some address and jump to other addresses in the program as needed via a GTO call. 😱 Above, we’re assuming the simple case of having but the one program, which starts at location 01, though convention is to GTO 00 any time you aren’t sure where the program counter happens to be pointing at the time, since location 00 is treated specially.

It would be bad enough that this program expanded from five active lines of Perl2 to twenty-nine program steps in the HP-12C, but it gets worse. Not only are we heavily dependent on evil GTO statements with tricky path dependencies, the 12C variant of HP RPN lacks approximately every other affordance of structured programming:

You may be wondering why this program counts backwards when the problem statement suggests that we proceed from 1 — or 3 if we’re being clever — and then stop either at 999 with a test at the bottom of the loop or against n+1=1000 as a precondition to continuing. It is because the 12C has only two logical tests, one of which is x=0?, leading us to count downward and take advantage of the fact that addition is commutative: the order we visit the numbers in the sequence doesn’t affect the result. Counting upwards requires us to keep the limit value around so that we can repeatedly subtract the current count from it to decide whether we’ve reached the end of our labors. This costs instructions, plus a register.

I did my prototyping on an HP-12C made after the switch from their custom CMOSC process to a standard 3 V process, which replicated the original 1981 timings out of a misguided desire to reassure users that their calculators were working hard on their behalf. The illogic in play was that if the calculator returned the answer too fast, testers became convinced the machine had cheated or malfunctioned in some manner. This horse-hockey design decision resulted in a running time in excess of 15 minutes for this trivial program on hardware made through 2007 and into early 2008!

Bleah!

There’s one easy way to improve on this: replace this ill-considered silicon with the second-generation ARM emulation — available since 2015 — which produces the correct result in about six seconds. 6 ms per iteration remains stupid-slow by the standard of any of its wall-powered contemporaries, but at least there’s a decent chance the human won’t get bored and wander off while waiting for the answer.

HP-11C

This scientific variant of the HP-12C was introduced to the market at the same time in 1981, but the design did not enjoy the same extraordinary longevity. I believe this is because the scientific market is always hungry for better machines owing to the steady march of progress in STEM fields, whereas the prototypical target user for a financial calculator has pretty much the same job as in 1981. HP did make a handful of improvements for these users along the way, but not out of any need to track changes in how, let us say, mortgage payoff schedules are calculated.

In place of bond yield and TVM functions, the HP-11C offers several features that combine to make it a significantly superior machine for writing programs, which allowed me to produce this variation on the above:

A:      f LBL A         # A = first Project Euler Problem
        g CL𝑥           # zero x without bumping stack
        STO 0           # zero R0 = initial accumulated total
        999             # starting iteration, down to 0
        STO I           # not abuse in 11C; only option
8:      f LBL 8         # 8 = cringy mnemonic = next *eight*eration
        RCL I           # retrieve iteration count
        3               # x % 3 == 0?
        ÷
        f FRAC
        g x=0?          # zero remainder?
        GTO 4           # yes; accumulate x
        RCL I           # retrieve iteration count
        5               # x % 5 == 0?
        ÷
        f FRAC
        g x≠0?          # nonzero remainder?
        GTO 1           # yes; skip accumulating x
4:      f LBL 4         # (4)th-row basic op = ADD, accumulate I
        RCL I
        STO+ 0
1:      f LBL 1         # common decrement-by-(1) step
        f DSE
        GTO 8           # more work to do; next *eight*eration!
        RCL 0           # we're done; copy R0 into x for display to user
        g RTN           # then end

The superior programming model results in a slightly tighter inner loop, yet it runs significantly slower on my HP-11C than on an original-production HP-12C, 17 minutes and 30 seconds. This is doubly disappointing in a world where we can get speedy 12C variants at much lower prices. Currently, the only options for making this specific program run truly fast are simulators/emulators or the SwissMicros DM11L.

Yet, we do benefit from several quality-of-life improvements in the 11C. Exemplified in this short program are:

Given these improvements, you may wonder why this program is but a single instruction shorter than the 12C version?

The largest single net cost to length is for the four labels we added, which I find well worth the freight. Human reading and understanding time matter, too, not just storage and runtime efficiency.

We also pay the cost of one additional RCL I in this version, up at the top of the main loop. The 12C version avoids it as a surprise bonus from doing the countdown manually, because there is no path to the top of the loop that doesn’t load I first. DSE is nice, but it doesn’t offer this same side effect.

HP-15C

The translation from the HP-11C program above to 15C form is trivial. It suffices to list the changes:

  1. Tests: The f x≠0? line becomes g TEST 0.3

  2. Increment: Because the 15C’s DSE operation works on any addressable register — not just 𝐈 as in the 11C — the instruction on program line 25 becomes f DSE 𝐈. (Logical line 23 in the above program listing, where we collapse the 999 loop constant onto a single line.)

    One may argue that continuing to use 𝐈 for this purpose constitutes register abuse since it precludes indirect addressing. We didn’t have a choice but to make that tradeoff in the 11C, but now that we have the option, you could choose to assign one of the other general-purpose registers to the countdown. The natural choice is R1 since we’re using R0 as our accumulator. I choose to accept the tradeoff in order to better draw the parallel, but then, I’m coming from a teaching perspective in this article, not a practical one.

    If you finish keying in this program and find that it is showing 27 instructions and not the 28 expected, this is what you likely missed: it was waiting for the referent to the DSE call, and you canceled it by moving on to the subsequent GTO 8 instruction.

For reasons I do not understand, the original-production HP-15C I have here runs this adjusted program ~15% slower than my 11C. While I could dig into the reasons for that and possibly find solutions, in practice what I’d do to speed it up is run it on more modern hardware.

HP-32S

It is my view that the logical successor to the 11C is the HP-32S. They’re both mid-range scientifics, neither super-powerful nor stripped down to meet a dubious pedagogical limitation set by a textbook review board. For a good many professional users, they’re “enough” calculator.

Everything that makes the 11C more pleasant to program than a 12C is improved in the 32S, allowing this variation:

P01     f LBL P         # (P)roject Euler Problem #1
        f CLEAR 𝑥       # zero x without bumping stack
        STO A           # zero A = initial accumulated total
        999             # starting iteration, down to 0
        STO C           # (C)ountdown for main loop
N01     f LBL N         # (N)ext iteration
        RCL C           # retrieve iteration count
        3               # x % 3 == 0?
        ÷
        f PARTS FP
        f TESTS x=0?    # zero remainder?
        f GTO A         # yes; accumulate x
        RCL C           # retrieve iteration count
        5               # x % 5 == 0?
        ÷
        f PARTS FP
        f TESTS x≠0?    # nonzero remainder?
        f GTO D         # yes; skip accumulating x
A01     f LBL A         # (A)ccumulate C
        RCL C
        STO+ A
D01     f LBL D         # common (D)ecrement step
        f DSE C
        f GTO N         # more work to do; (N)ext iteration!
        RCL A           # we're done; copy A into x for display to user
        f RTN           # then end

I continue to use the classic shift name f here even though HP changed it to an 🟧 button on the 32S. My first reason is that switching to the Unicode orange box character breaks the monospace alignment in the font I happen to be using here as I type this. My second reason is better: consistency aids comparison to the above versions.

You may notice that the transformations listed above also appear here. Although the 32S is not a proper successor to the 15C4 essentially the same reasons apply. The primary exception is that we must use an on-screen TESTS menu to pick the logical operation instead of flipping the calculator over and looking up its numeric code.

A more obvious difference relative to the 11C and 15C versions is that the 32S only supports alpha labels, permitting use of all 26 in the English alphabet, plus the special-cased i register, distinct from the general-purpose I register. That’s one better than the 26 in the 11C/15C,5 but the mnemonic improvement is the real draw. You see this in the use of A in this version: once as a variable to hold the (A)ccumulated total and once as the label of the subroutine that (A)dds the current iteration value to that total.

The straight-line option on the table here is whether to switch from the special-cased 𝐈 register we used in the 11/15C versions to either i or I, but I chose to avoid this confusing mess entirely by switching to C, for reasons best explained later.

That brings us to one last noteworthy change: the 32S can store the 999 iteration loop constant within a single instruction, meaning the line numbers above accurately reflect how it shows on the calculator. The parallel would be even clearer if I bothered to write out the P02… and subsequent labels after each subroutine, but I have chosen instead to maintain the scheme of showing the label only for jump targets, to make them stand out and to reduce pointless differences between versions of this program. I did change from the A: scheme used above for the program’s entrypoint to P01 to match how the calculator displays it in PRGM mode, however.

My HP-32S runs this program in 1 minute and 9 seconds.

HP-32SII / SwissMicros DM32

The 32S version above works as-is, but several of the functions moved about on the keyboard:

  1. 🟧 PARTS moved to a 🟦 shifted menu above the √𝑥 key.
  2. DSE became a dedicated operation on the 🟦 shifted 9 key, no longer buried in the LOOP menu.
  3. The TESTS menu split into 🟧 and 🟦 shifted operations above the ÷ key.

Several other functions also moved about, but since they remain 🟧-shifted, I do not bother to list them here.

I do not (yet?) have a HP-32SII, but I am reliably informed that it takes about 30% longer to run things for some reason. 1.5 minutes for this program, ballpark.

My SwissMicros DM32 blazes through this task in a bit over a second while on battery power. Switching to USB-C power cuts that in half since it allows for an increase in CPU clock rate.

HP-35s

Essentially the same program also works on an HP 35s here, but because there are many keyboard layout changes, I felt it was worth giving the “flattened” version here rather than a list of diffs against the 32S version:

P001    g LBL P         # (P)roject Euler Problem #1
        g CLEAR 𝑥       # zero x without bumping stack
        g STO A         # zero A = initial accumulated total
        999             # starting iteration, down to 0
        g STO C         # (C)ountdown for main loop
N001    g LBL N         # (N)ext iteration
        RCL C           # retrieve iteration count
        3               # x % 3 == 0?
        ÷
        g INTG FP
        g x=0?          # zero remainder?
        GTO A001        # yes; accumulate x
        RCL C           # retrieve iteration count
        5               # x % 5 == 0?
        ÷
        f INTG FP
        g x≠0?          # nonzero remainder?
        GTO D001        # yes; skip accumulating x
A001    g LBL A         # (A)ccumulate C
        RCL C
        g STO+ A
D001    g LBL D         # common (D)ecrement step
        g DSE C
        GTO N001        # more work to do; (N)ext iteration!
        RCL A           # we're done; copy A into x for display to user
        f RTN           # then end

Those of you who have a 35s at hand are now in a position to guess why we chose to switch to C for the loop control variable.

Go on, take a squiz at its keyboard.

Do you see the problems taking the straight-line option we were presented above would bring? There are two:

  1. HP renamed the indirect-addressing register 𝐈 to i in the 32S to avoid a collision with the general-purpose I register, but the 35s effectively reverts that by offering the choice of the otherwise general-purpose I or J registers for indirection. We should therefore reserve these for that purpose when we have the option.

  2. They did that because the 35s adds a different i key for entering complex numbers. If you try saying STO i on a 35s, what you actually get is STO G!

    A prior version of this article took that as a divine hint from the ghosts of Bill Hewlett and Dave Packard that I should use G for the inte(G)er portion of the iteration variable, but on optimizing these programs to do away with the fractional part of the ISG (now DSE) control variable, I gave up on that cutesy mnemonic.

Shockingly, the 35s takes 2½ minutes to run this program even though it is step-for-step equivalent to the 32S version. A desire for battery savings doesn’t explain it; the 35s runs on a paralleled pair of CR2032s, while the 32S battery is a series-connected trio of LR44s, which gives the 35s roughly twice the usable battery capacity.6 In the 19 years between these designs’ market introductions, processors not only got much more efficient, the optimal path to overall battery savings shifted to a “race-to-idle” paradigm: the faster a task completes, the faster the CPU can drop into its lowest power mode short of being asleep or completely off. Running at a lower clock rate does save overall power, particularly when the sleep mode is inefficient, but one only has to look at a rather more famous 2007 product introduction — the iPhone — to know the 35s designers were not operating under all the same historical design constraints.

If I come across as down on the 35s, I’m not. While it tends to drop off the end of short lists of favorite RPN calculators, like, evar, I will not hesitate to say there’s a lot to like and admire about it. Here’s one pure win: when Kinpo designed the 33s, they added the long-awaited “remainder” function, which in their follow-on 35s design cuts the program’s running time down to two minutes flat via a net reduction of two instructions in the core loop. Simply drop the explicit ÷ steps and change the FP steps to f INTG Rmdr, then enjoy the savings!

HP-42S

I do not own an HP-42S, and I doubt I ever will short of finding one by accident that is being sold to cheaply to pass by. Even so, I found myself needing to familiarize myself with this 1988 classic as part of my journey toward enlightenment with my shiny new R47, one of the first batch made publicly available in late 2025. It and the assortment of community-developed ancestors it descends from are all based loosely on the 42S, and the parallels are most clearly visible in the programming model.

My primary goal in taking up the Project Euler Problem #1 challenge once again is to minimize the differences relative to the HP 35s version while showing the key improvements in the 42S programming model, greatest of which is multi-character alpha labels for programs and variable names. Secondarily, I wish to use this as a stepping-stone to the R47 version below. These constraints are meant to keep the diffs below minimal for best clarity.

Ultimately, all I found needful to translate the 35s program is to:

  1. Rename all the GTO jump points and variables in accord with its new rules.

    Global names are those surrounded by double-quote marks, and local names are either:

    • single-letter and limited to A-J, a-e
    • numbered, 00-99

    To maintain a parallel with the 35s version of this program, my 42S version uses capital-letter local labels.

  2. Add an “END” instruction. The 42S adds the concept of local GTO/XEQ labels, whereas in all earlier machines covered above, the equivalent was implicit and singular: the end of program memory. All programs loaded into a given HP-42S share a single global namespace, but each one encloses a separate, wholly independent local namespace delimited by these END instructions.

This is the result:

PEP1    LBL "PEP1"      # (P)roject Euler Problem #1
        f CLEAR CLX     # zero x without bumping stack
        STO "A"         # zero A = initial accumulated total
        999             # starting iteration, down to 0
        STO "C"         # (C)ountdown for main loop
I       LBL I           # next (I)teration
        RCL "C"         # retrieve iteration count
        3               # x % 3 == 0?
        ÷
        f CONVERT FP
        x=0?            # zero remainder?
        f GTO A         # yes; accumulate x
        RCL "C"         # retrieve iteration count
        5               # x % 5 == 0?
        ÷
        f CONVERT FP
        x≠0?            # nonzero remainder?
        f GTO D         # yes; skip accumulating x
A       LBL A           # (A)ccumulate C
        RCL "C"
        STO+ "A"
D       LBL D           # common (D)ecrement step
        DSE "C"
        f GTO I         # more work to do; next (I)teration!
        RCL "A"         # we're done; copy A into x for display to user
        RTN             # then return to this program's caller
END                     # terminate program's local space

SwissMicros R47

Although the R47 contains the HP-42S capabilities as one of its several subsets,7 it vastly expands on it, resulting in a large number of operational differences. The primary decision we must make here — given our wish to minimize the differences in the interest of keeping the base-level differences clear — is what to name the housekeeping variables:

I chose the second option to keep the diff size low, in the interest of drawing clean parallels between the versions.

The keyboard layout on the R47 diffs greatly from that of the HP-42S, and there are many menu rearrangements besides, but it is straightforward to chase most of these down. The only one I had any trouble finding is that the FP function is now under the 🟦 REAL menu instead of 🟧 CONVERT.

This done, the programs end up very nearly identical, differing mainly in cosmetic inessentials. That is due in large part to my choice not to take full advantage of the R47’s greatly expanded feature set. Those of a different mind might enjoy this thread on the SwissMicros forum.

Essential Differences

I have checked in all eight solutions8 as versions of a single file, pep001/sequence. By grace of Fossil’s version control features, this allows us to produce interesting diffs, showing the essential changes between the versions at a textual level:

Notice that from the 11C on, the number of program lines and their content remains functionally the same, as do the comments so long as they refer purely to the algorithm and not to the register and subroutine names that had to change along the way. Annoying keyboard and menu rearrangements aside, we see little else through this sequence than improvements to the programming model. One of the significant results of presenting these programs in this manner is that it distills pages and pages of feature lists to single screenfuls of practical differences.

To get started on your own explorations, visit the file information page and then click any two bubbles on the timeline. The first one clicked becomes the baseline, and the second shows the diffs relative to it. For instance, notice how minor are the differences between the 11C and 35s versions. That spans 26 years of development, yet once you pare away the inessentials, you find the same solution in a more sensible presentation.

We took this option to skip around above for the HP-42S version by comparing against the HP-32SII version to draw a better parallel in the programming models. Not only are these direct family mates in a way that the much later 35s is not, they're both first-party HP designs, and it shows. The sequence in version control has the 42S descending from the 35s instead, which brings in Kinpo’s LBL-relative GTO innovation. The resulting diff is consequently noisier.

Benchmarks

I collect here the running times of the programs above, done on as wide a variety of hardware as I have access to. I use the original-production HP-12C as my baseline even though it is not the slowest of the bunch, because it is the least capable on general-purpose tasks like this one. It’s one of those "If a pig flies, no one laughs if it fails to stick the landing” kinda things.

Device Under Test Seconds Speed
HP-15C (original) 1240 0.73×
HP-11C (original) 1050 0.87×
HP-12C (original) 910 1.00×
HP 12c Platinum 400 2.3×
DM15 @ 12 MHz 245 3.71×
HP 35s 210 4 ⅓×
HP-32SII (est.) 90 10.1×
HP-32S 69 13.2×
DM15 @ 48 MHz 56 16 ¼×
HP-15C CE 8 114×
R47 on battery 7 ½ 121×
HP-12C (ARM v1) 7 130×
HP-12C (ARM v2) 6 152×
R47 on USB-C 2 455×
DM32 on battery 1 ½ ludicrous
DM32 on USB-C ¾ plaid

These times are accurate roughly to the second, with the error term owed to me getting effin’ bored watching the LCD blink running at me, continually evaporating the willpower needed to keep my finger hovered over my timer’s Stop button, then rouse quickly from my daydream when it finally displays the answer. If I was a clever person, I’d put a video camera over the test bench and set it running while I went and did something productive, but alas… This is what happens when you seek out an amateur on the Internet to do your benchmarking for you.

Fractions indicate rough approximations, where I do not wish to misuse decimals to imply possession of more precision than I do in fact have. It’s like mama’s cooking: everyone eats hearty and thanks her afterward without even thinking to quibble about how she heaps the sugar measure instead of carding it off smooth like the book recommends.

Notice that the more advanced calculators tend to run slower, all else being equal. (HP-12C faster than HP-11C, DM32 faster than R47…) This is generally because of these advances; more code run per instruction on identical hardware gives a slower result when one of the goals is to keep the programs as similar as possible, as we find here. Taking better advantage of the more advanced functionality can swing the needle back the other way, as we saw with the Rmdr alternative for the HP 35s version above.

Thanks

On presenting this article to the HP Museum Forum, I received a good amount of useful feedback, which informed several revisions. The largest single contribution is from Thomas Klemm, who cut ~30% of fat from my initial attempts without changing the essence of the algorithm. While that makes the program run faster, the primary value I see in this is that it puts the entire program onto any sensible screen without vertical scrollbars. That is true even of a modern smartphone in portrait orientation, though alas with horizontal scrollbars due to the extensive comments.

You may find other ideas in that thread interesting, but I choose to stop here because my purpose is to set up a way of comparing programming systems and drawing interesting conclusions therefrom, not to find the answer to a question approximately no one needs answered.

License

This work is © 2025-2026 by Warren Young and is licensed under CC BY-NC-SA 4.0


  1. ^ Disqualifiers: being made by another company, not having an RPN mode, and not being programmable.
  2. ^ That’s discounting the shebang, the } alone on a line, and cuddling the logical test up onto a single line.
  3. ^ This obscurity is one reason I prefer the nicely-balanced 11C to the overloaded 15C, whose four additional numeric test operations would further crowd its keyboard if HP had made them first-class functions as on the 11C. To alleviate the pressure, HP hid six of the tests away behind a new g TEST function, then added these four more and documented them in a table on the backside of the calculator; you must flip the 15C over to look up the single-digit code for all but two tests. (The same two tests the HP-12C is limited to, incidentally.) With the 11C, half are printed right there on the key caps as g-shifted operations, the other half above those same four keys on the faceplate as f-shifts. The 32S shows a superior solution to the too-many operations problem: collect them into menus. Imagine a 15C with a 5-item menu system picked via the A-E keys. Better, imagine one with A-F keys to allow for a hex mode like the 32S. Mmm; me want!
  4. ^ The HP-42S holds a better claim to that title.
  5. ^ 5 alpha labels, plus 𝐈, plus the 20 directly-accessible numbered registers R0-R9 and R.0-R.9.
  6. ^ Both cell types are in the 200 mAh neighborhood, but parallel cells add their mAh ratings while a series connection gives higher voltage at the same mAh of each single cell making up that battery. This, incidentally, is why EE-trained types like me become annoyed at those who refer to a single CR2032 cell as a “battery.” We aren’t being pointlessly pedantic; the distinction materially matters to us.
  7. ^ Most notably, the R47 also subsumes the capabilities of the HP-12C and HP-16C, which were off to the side of the main evolution line above.
  8. ^ That counts the 15C and 32SII variants not shown explicitly above, being too minor to be worth scrolling through.