Bash luhn exercise

I’m attempting the luhn exercise. I’m finding this one tricky in places. I have most of it working, but there are some areas that aren’t working correctly.

It calls for each 2nd digit of the input to be doubled starting from the right.

I have a for loop that loops through the input, but doesn’t stop once it hits 0:

read string #4539319503436467
for (( x=${#string}; x <= ${#string}; x-- )) do...

This keeps going past 0 into negative numbers. I have put an if/break statement after it to stop it once it hits 0, but this adds unnecessary complexity to it.

Then, the logic for making it double every 2nd digit is placed after that for loop. Using 4539319503436467 as the input:

`if [[ $x < ${#string} && $x > (($x - 2)) ]] then...`

This doesn’t work at all, and produces strange results.

I’m not sure what’s going on, especially with that for loop, which seems pretty standard. Any suggestions are appreciated.

Focusing on the for loop, can you describe what values x takes on and when/why the loop should end? If the string is 3 characters, what values should x have? When and why should the loop terminate?

Your questions jarred my thoughts, and I came up with a solution that works.

x takes on ${#string}, which is a 16 digit string of user input. Then while x is greater than or equal to 0, the loop runs. It terminates when x equals 0:

for ((((x=${#string}-1)); x>=0; x-- ))
do
  echo ${string:$x:1}
done

I added the -1 because for some reason it was printing blank for the 16th position. Not sure if there’s a better way.

That looks a lot better.

  1. If the lower value is 0 and you want n values, then the highest value should be n - 1. For instance, to have two values, you want 1 and 0, so you start at n - 1.
  2. The inner pair of (( is not needed.
  3. Did you need any further assistance here or did you work out the rest?

Remember,

The first step of the Luhn algorithm is to double every second digit, starting from the right.

Counting from the left may cause issues.

I unfortunately cannot get the logic for doubling every 2nd digit from the right to work:

If the input is a 16 digit number: 4539319503436467, and is processed from the right:

if [[ $x < ${#string} && $x > ((${#string} - 2)) ]]
then...

I’m trying to say: if x is less than 16, (which would be the 2nd digit from the right), and x is greater than 16 - 2, (which would be the 3rd digit from the right). But I can see that it wouldn’t work through the rest of the input string. I think I need a way to minus 2 through the string.

Can you think of a way (logically, not using code ) to identify which values of x (the index) should be doubled for inputs 1234 and 12345?

The values of x (the index) would be:

Input: 1234
Index: 1,3

Input12345
Index: 2,4

Yes. That’s the correct indices. Can you think of a way to describe them and/or explain how to compute them (using English and not code)?

All I can think of is that it’s “every 2nd indices from the right.”
And “if true, add it to itself.”

I’m not sure how to translate that into code. Every time I try, I end up with something like I tried in an earlier post:

if [[ $x < ${#string} && $x > ((${#string} - 2)) ]]
   then
temp=$((x + x))
echo "temp is: $temp"

I’ve tried variations on it like:

if [[ $x < ${#string} && $x > ${#string} - 2 ]]

or:

if [[ $x < ${#string} ]] && $x > [[ ((${#string} - 2)) ]]

These don’t work at all.

Can you describe that numerically somehow? “From the right” is a very vague term. Can you describe that in terms of how someone could compute those numbers?

Instead of “from the right”, I would say “from ${#string}”, which is the max of the user input.

Seems like it would be some form of counting by two’s down from ${#string}

4 5 3 9 3 1 9 5 0 3 4 3 6 4 6 7 - user input
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 - indices

UPDATE:

So I got the logic to work, this computes every 2nd digit from the top of ${#string}:

echo "Please enter a number/string: "
 read string
 set -x
   
     for (( x=${#string}; x >= 0; x-=2 ))
  
     do

         echo "x is:$x"
         echo "string is:${string:$x:1}"
         
      done

I always new in my head what to do, I just didn’t know the syntax. -= was the key.

Now I’m trying to simply add the value of string:$x:1 to itself. I’ve tried:

temp=$(( ${string:$x:1} + ${string:$x:1} ))
echo "temp is $temp"

Which I thought was pretty obvious, but I get operand expected, syntax error.

Any idea?

Works for me.

» string=123456
» x=4
» temp=$(( ${string:$x:1} + ${string:$x:1} ))
» temp=$(( ${string:$x:1} + ${string:$x:1} ))
» echo "temp is $temp"
temp is 10

You can use set -x to debug. You can also use temp=$(( ${string:$x:1} *2 )) to double a number :slight_smile:

Weird. I can’t get it to work.
This is what I’m running in Vim:

#!/bin/bash
 
   echo "Please enter a number/string: "
   read string
   set -x
   
     for (( x=${#string}; x >= 0; x-=2 ))
  
      do
   
         temp=$(( ${string:$x:1} + ${string:$x:1} )) 
         echo "temp is $temp"
                             
     done

And I get this:

Please enter a number/string: 
4539319503436467
+ (( x=16 ))
+ (( x >= 0 ))
./luhn_test.sh: line 11: +  : syntax error: operand expected (error token is "+  ")

I’ve tried it in nano also, still doesn’t work.

When x is 16, what is ${string:$x:1}?

${string:$x:1} is blank at the 16th position of ${#string},
but that’s because it gets skipped because it’s not a -=2 position, and/or because the index runs from 0-15. I think it’s because the latter.

I think I got it:

#!/bin/bash
   
   echo "Please enter a number/string: " #4539319503436467
   read string
   #set -x
   
    for (( x=${#string}-2; x >= 0; x-=2 )) 
     
      do
        echo "x is:$x"      
        echo "string is:${string:$x:1}"  
        echo=$(( ${string:$x:1} *2 )) 
          
     done

The strange thing is the last echo statement only works if I use set -x.
Something looks wrong about it, but I can’t figure it out.

You mean this line? This is a variable assignment. It does not run the echo command.

So any digit in the input after it’s doubled, greater than 9, gets 9 subtracted from it, and I am trying to get those numbers to be populated into an empty array. I’ve used this syntax before:
array=($($temp - 9 ))

but it’s not working here. Gives “command not found” errors.
Any idea why?


#!/bin/bash
   
   echo "Please enter a number/string: " #4539319503436467
   read string
   #set -x
   array=()
   
     for (( x=${#string}-2; x >= 0; x-=2 )) 
      
       do
  
          echo "x is:$x"     
          echo "string is:${string:$x:1}" 
          temp=$((${string:$x:1}*2))
  
           if [[ $temp -gt 9 ]]
  
             then
                     array=($($temp - 9 ))         
           fi
  
       done
  
       declare -p array
  

Have you tried that in a shell? $($temp - 9) expands temp then tries to run that command. That is a command substitution. You might be confusing that with $(( temp - 9 )) with a double parenthesis which does arithmetic substitution. That also sets array to a list with a single element. If you only have one element, you don’t especially need an array.

I"m trying to get each character of the input into an individual element of an array, over each pass of a loop.

input - 4539319503436467

I’ve tried:

array+=$(({string:$x:1}))

and

array+=${string:$x:1}

But that just adds all of the input into one element:

declare -a array=([0]="7646343059139354")

Any suggestions?

array=()
for ...; do
    array+=( "$element" )
done

You can’t use arbitrary syntax and expect it to know what you want it to do ;)