49 Issues and ChatGPT

The New Yorker publishes every Monday. However, they have 4 issues each year that are double issues, each covering 2 weeks.

The first issue for 2024 is a Jan. 1/Jan. 8. I wondered whether the publisher’s decision was 48 issues a year or 4 double issues each year. I thought it would be interesting to know which years they have a choice.

In other words, I wondered what years in the 21st century have 53 Mondays in instead of the regular 52. When I asked ChatGPT 4 for the answer, I was surprised at how it found its answer.

Rather than having specific date training that could be consulted, ChatGPT wrote some Python code. It then ran the code giving the answer of 2001, 2007, 2012, 2018, 2024, 2029, 2035, 2040, 2046, 2052, 2057, 2063, 2068, 2074, 2080, 2085, 2091, 2096. If you look at the list, the years are spaced out every 5 or 6 years.

The code was:

from datetime import datetime, timedelta

# Function to check if a year has 53 Mondays
def has_53_mondays(year):
    # January 1 of the year
    jan_1 = datetime(year, 1, 1)
    # Count the number of Mondays
    monday_count = 0
    for i in range(365 + (jan_1.year % 4 == 0 and jan_1.year % 100 != 0 or jan_1.year % 400 == 0)):
        if (jan_1 + timedelta(days=i)).weekday() == 0:
            monday_count += 1
    return monday_count == 53

# List of years in the 21st century with 53 Mondays
years_with_53_mondays = [year for year in range(2001, 2101) if has_53_mondays(year)]
years_with_53_mondays

It’s understandable with a little work and doesn’t make any sophisticated assumptions. To get the answer, it visits every day so that it takes about 36525 steps to get the 21st century result. (With a deeper understanding of the calendar, it wouldn’t need the inner loop). This code offered a solution that is verifiable. (I just did a little research and Python’s calendar library doesn’t account for 1752.)

I only checked a few of its answers but the code looks robust.

I was curious what else it could do. I asked it for the first prime power greater than 1000000. The tool wrote some code and gave what looked like a reasonable answer.

# To find the first prime power greater than 1,000,000, we start by finding prime numbers and then checking their powers
# until we find a value greater than 1,000,000.

from sympy import primerange, isprime

def first_prime_power_above(limit):
    for prime in primerange(2, limit):
        power = 2  # Start from the square of the prime
        while True:
            prime_power = prime ** power
            if prime_power > limit:
                return prime_power
            power += 1

# Setting a limit for the search range. The exact limit is a guess; we need a range large enough to find our number.
search_limit = 10000
first_prime_power_above(1000000)

The code has a problem because it will only return a power of two. It only exits the inner loop with “return” and thus only analyzes powers of 2.

Armed with that understanding, I saw that it would fail on 150, so when I asked, it gave me 256. I knew 169 was the right answer. When I pointed out that it was using a greedy algorithm that only returns a power of two. It generated new code, below. It uses Python sets to calculate the result.

# Revisiting the approach to correctly find the first prime power greater than 150, considering all primes and their powers.

def first_correct_prime_power_above_v2(limit):
    prime_powers = set()
    for prime in primerange(2, limit):
        power = 2
        while True:
            prime_power = prime ** power
            if prime_power > limit:
                prime_powers.add(prime_power)
                break
            power += 1
    return min(prime_powers)

first_correct_prime_power_above_v2(150)

I was surprised that ChatGPT would generate code to make its analysis in both cases. The team implementing the engine saw that Python works as a lingua franca to implement algorithms. Simple Python is popular and has clean syntax.

My experiment demoed a feature that I didn’t expect. I can validate the code. ChatGPT lets me look under the hood while the textual responses won’t allow. It also shows that even unambiguous mathematical questions can yield incorrect results.

Bard was not useful. On the date question, it gave a list of incorrect answers. Rather than having a verifiable algorithm, it hallucinates a rule to explain its answer. Often its verbal algorithm isn’t correct and may not even make sense. Nor does the answer even follow the heuristic. I pointed out one of the mistakes in its year list and it just came up with a different confused rule it didn’t follow and got an even worse answer. I’ve noticed that Bard is inflexible and will double down on wrong answers.

For the prime power, Bard gave nonsensical steps to explain what it calulated. And then didn’t use that procedure to find the answer.


The first prime power greater than 150 is 169.

Here's the reasoning:

1. Start with the closest prime number after 150, which is 151.

2. Check if 151 is a prime power: 151 is only divisible by 1 and itself, making it a prime number, but not a prime power (prime powers must be raised to an exponent greater than 1).

3. Move on to the next prime number, which is 157. Again, 157 is only divisible by 1 and itself, confirming it's prime.

4. However, 157 raised to any positive exponent will still be an odd number, so it can't be the first prime power greater than 150.

5. Continue this process with the next prime numbers: 163 and 167.

6. 163 is not a prime power (163 = 7 * 23).

7. Finally, 167 is both a prime number and a prime power: 167 = 7^2, fulfilling both criteria.

Therefore, 169 (7^2) is the first prime power greater than 150. 

Bard offered two alternate responses, but they are just as jumbled and confused as this one. Step 4 is irrelevant. Step 6 is worse because 163 is prime. I don’t know why it has 167 and 169 confused nor why it uses seven squared.

Rather than lean on the English language to answer a mathematical question, ChatGPT uses an expertise in programming to answer math adjacent questions. This experiment demonstrates that it has basic algorithmic reasoning capabilities but is not consistently correct.

Going back to the original question, when I looked at a blow-in ad card, it said there are 4 double issues, so this year, I expect 49 issues with 4 of those being double issues.

A Simple Hue Transformation

A while ago I needed a way to calculate colors. I wanted an easy way to get bright colors. I wanted it to be simple, taking a single hue parameter and mapping it into an RGB color.

The solution I found is to choose the red, blue and green channel values as overlapping trapezoids. The color is supposed to be cyclic, so I could used the fmod function in C++ or the % remainder operator in JavaScript. The result would transform any floating point argument into the base domain of\(\lbrack 0, 1)\).

Color map from zero to 1

The algorithm picks the values according to the above graph. As the hue argument moves from zero to 1, each color has a section where it is at its maximum value, part where it is zero. Between the two extremes is a linear transition. A zero value maps to zero in the corresponding RGB component while a 1 gets mapped to 255.

I’ve tried different curves for the transitions and the linear slope seemed to be the best compromise to make bright colors.

What I created is diagrammed here

This shows the colors broken into the individual red, green and blue channels.

By changing the shape of the basis curves, different results can be obtained. This trapezoidal function was chosen in the end because it gives really bright colors and is simple to implement. (Also, I have a long-ago program that already uses this design and if I want to replicate it online, I need the same algorithm.)

The following code in JavaScript calculates the function.


var color = (function() {
    const coords = [ 0, 1, 1, 1, 0, 0, 0];
    const factor = 6;
    const red = 0;
    const green = 2/3;
    const blue = 1/3;

    function fromHue( hue ) {
        hue = ((hue % 1) + 1) % 1;
        const index = Math.floor( hue * factor );
        const fraction = hue * factor - index;

        return coords[index + 1] * fraction + 
                        coords[index] * (1 - fraction);
    }

    return function( hue ) 
    { 
        let r = Math.round( 255 * fromHue( hue + red ) );
        let g = Math.round( 255 * fromHue( hue + green ) );
        let b = Math.round( 255 * fromHue( hue + blue ) );
        if( r !== r || g !== g || b !== b ) { 
             r = g = b = 0; 
        }

        return `rgb(${r},${g},${b})`;
    }
})( );

The coords array specifies the function’s value at each of the evenly spaced corners. The seventh value matches the first so that the curve is cyclic. Each hue that is not at a vertex will be a linear interpolation of the two quantities to the left and right. You can verify the values in the coords array by reading the red graph at the marked points on the hue axis. The coords table adds flexibility because different coords array and adjusting “factor” to match it can create other spectra.

Each color is out of phase by 1/3, Thus adding 1/3 and 2/3 to the hue will shift the graph so that the same trapezoidal curve can be used for each of the red, green and blue channels. Blue is 1/3 rather than green because if you shift the blue graph to the right by 1/3, it lines up with the red graph while green must shift right by 2/3 to match.

Inside the fromHue( ) function, the “((hue % 1) + 1) % 1” expression looks peculiar. The % operator returns the remainder of a division. For positive hue, the remainder of dividing by 1 is the fractional part. If hue is negative, the remainder will negative, between -1 and 0. Adding 1 and then calculating the remainder of that will map the entire domain for each color channel to the range [0, 1) without needing an “if.” By mapping hues into that range of 0 to 1, it is as if the above diagram repeats indefinitely to the left and right along the hue axis.

The Math.floor() operation takes a hue times six and converts it into an integer index for the coords array. By multiplying by 6, the corner points have integer indices and the fractional part left by subtracting index can be used to do the interpolation.

In the return statement of fromHue( ), “index + 1” accesses the value of the function to the right of the calculated fraction. Since the points of the coords array are 1 apart, the slope is \(coords\lbrack index + 1\rbrack – coords\lbrack index\rbrack\)

In point slope form, the value is

$$coords\lbrack index\rbrack + (coords\lbrack index +1\rbrack – coords\lbrack index \rbrack ) * fraction$$

Combining the two references to \(coords\lbrack index \rbrack\) results in the expression above.

After calling fromHue(), the calls to Math.round( ) take each component and map it to an integer from 0 to 255. The text that it returns is appropriate for using for the color in styles and graphic contexts.

The final “if” statement detects when the results are NaN and replaces that with black. NaN is the only value that is not equal to itself. Whenever the argument hue is not a number, fromHue( ) will return NaN.

For example, if hue were 1.6, for Red, the first expression would replace hue with 0.6. Then, index would be Math.floor( 6 * 0.6 ); or floor( 3.6) or 3. Fraction would be 0.6. Coords[ 3 ] is 1 and coords[4] is 0. The result then is 0 * 0.6 + 1 * (1 – 0.6) which evaluates to 0.4. Then, red would be set to Math.round(255 * 0.4) which is 102. For Green, fromHue would be given about 2.267 which changes to 0.267 when you take the remainder. Index would be Math.floor( 6 * 0.267 ) or floor( 1.6 ) or 1. Coords[1] and coords[2] are both 1 so the return statement would return 1. This would then become 255. For Blue, fromHue would be given about 1.933 which would become 0.933. This leads to an index of 5 and fraction of 0.6. Coords[ 5 ] and coords[ 6] are both 0 so blue would be 0. Since none of these are NaN, the result would be the string “rgb(102,255,0)”

Because the corners in the trapezoidal shapes are positioned at constant intervals, the algebra is simplified. Without a repeating shape or with unevenly spaced key points, the algorithm would need an inelegant sequence of ifs to calculate the curves.