Solving a (simple) numeric exercise in bash - bash

Some of you are probably familiar with Project Euler, and I'm currently attempting a few of their problems to teach myself some more bash. They're a bit more mathematical than 'script-y' but it helps with syntax etc.
The problem currently asks me to solve:
If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000.
The code I have looks like so:
#!/bin/bash
i="1"
for i in `seq 1 333`
do
threes[$i]=`calc $i*3` # where 'calc' is a function written in bashrc
#calc actually looks like: calc() {awk "BEGIN { print "$*"} }
let "sumthrees = sumthrees + ${threes[$i]}"
done
for i in `seq 1 199`
do
fives[$i]=`calc $i*5`
let "sumfives = sumfives + ${fives[$i]}"
done
let "ans = $sumfives + $sumthrees"
echo "The sum of all 3 factors is $sumthrees and the sum of all five factors is $sumfives"
echo "The sum of both is $ans"
#So I can repeatedly run the script without bash remembering the variables between executions
unset i
unset fives
unset threes
unset sumfives
unset sumthrees
unset ans
So far I've not gotten the correct answer, but have run out of ideas as to where I'm going wrong. (FYI, the script currently gives me 266333, which I believe is close, but I don't know the answer yet.)
Can anyone spot anything? And for my own learning, if there are more elegant solutions to this that people might like to share that would be great.
EDIT
Thanks for all the answers, super informative. Since there are so many useful answers here I'll accept my favourite as the proper thread answer.

Blue Moon pointed out the actual problem with your logic.
You don't need to store all the threes and fives in arrays because you don't need them later.
You don't need to unset variables at the end of a script if you use ./yourscript or bash script because they'll disappear along with the
shell instance (better to initialize them first in any case).
You don't need awk to do math, bash does that just fine.
seq and let are not the best way to do anything in a bash script.
Here's a straight forward version:
#!/bin/bash
sum=0
for ((i=1; i<1000; i++))
do
if (( i%3 == 0 || i%5 == 0 ))
then
(( sum += i ))
fi
done
echo "$sum"

Your logic is almost right except that there are numbers which divide by both 3 and 5. So you are adding these numbers twice. Hence, you get wrong answer.
Use another loop similar to ones you have and subtract the ones that divide by both 3 and 5 from the result.

A few tips you might find useful:
In bash, you use let to give the shell a hint that a variable should be considered a number. All bash variables are strings, but you can do arithmetic on numerical strings. If I say let i=1 then i is set to 1, but if I say let i="taco" then $i will be 0, because it couldn't be read as a number. You can achieve a small amount of type-safety when doing mathematical work in the shell.
Bash also has $((this)) mechanism for doing math! You can check it out yourself: echo $((2 + 2)) -> 4, and even more relevant to this problem: echo $((6 % 3 == 0)) -> 1
In case you aren't familiar, % divides the first number by the second, and gives back the remainder; when the remainder is 0, it means that the first is divisible by the second! == is a test to see if two things are equal, and for logical tests like this 1 represents true and 0 represents false. So I'm testing if 6 is divisible by 3, which it is, and the value I get back is 1.
The test brackets, [ ... ] have a "test for equality" flag, -eq, which you can use to check if a math expression has a certain value (man test for more details)!
$ let i=6
$ echo $((i % 3 == 0 || i % 5 == 0))
1
$ if [ $((i % 3 == 0 || i % 5 == 0)) -eq 1 ]; then echo "yes"; fi
yes
(|| is another logical test - $((a || b)) will be 1 (true) when a is true or b is true).
Finally, instead of doing this for the number 6, you could do it in a for loop and increment a sum variable every time you find a multiple of 3 or 5:
let sum=0
for i in {1..1000}; do
if [ $((i % 3 == 0 || i % 5 == 0)) -eq 1 ]; then
let sum=$((sum + i))
fi
done
echo $sum
And there you'd have a working solution!
Bash has a lot of nice little tricks, (and a lot more mean ugly tricks), but it's worth learning at least a handful of them to make use of it as a scripting tool.

How about creative use of the modulus function & some checks. Then you have just 1 loop.
#!/bin/bash
i=1
while [ $i -lt 1000 ]
do
if [ $(($i % 3)) -eq 0 ] || [ $(($i % 5)) -eq 0 ]
then
sumall=$(($sumall+$i))
fi
i=$(($i+1))
done
echo "The sum of both is $sumall"
Answer: 233168

A different solution:
#!/bin/bash
sum=0
for n in {1..999}; do [ $(((n%5) * (n%3))) -eq 0 ] && sum=$((sum+n)); done
echo $sum
The script loops through all numbers below 1000, tests if the product of the number mod 3 and the number mod 5 is 0 (the product of two numbers can only be zero if one of them is zero). If that is the case, it adds the current number to a sum, which is printed out afterwards.
By the way, if I were you I'd include the definition of the calc function inside the script, to get a self-contained solution that doesn't need your specific configuration.

Related

Bash how to minus the number of guesses each time the user inputs?

while true
do
if [ $userinput = 1 ];
then
guesses=10
(( answer = RANDOM % 20 ))
read -p "Guess the number between 1-20 if you can $answer : " input
if [ $input != $answer ];
then
(( guesses=guesses-1 ))
echo "Wrong answer! You got ${guesses} left!"
else
echo "Correct answer! You had ${guesses} left. Lucky you!"
read -p "${name}, would you like to continue playing or not [Yes/No]? " decide
if [ $decide = "Yes" ];
then
continue
else
echo -e "${Red}Bye bye!"
Example:
guess=10
User inputs 2 guesses then program has to minus those 2 guesses from total of 10 guesses, in that case, 10-2=8 guesses left. How to do this?
The only necessary change is moving guesses=10 out of your loop, such that it's run only once (when your script is starting).
As for best-practice decrement forms, a terser bash-only approach (albeit no more or less valid than your existing implementation) would look like:
(( guesses-- ))
...whereas a more portable approach (compatible with all POSIX-family shells) is:
guesses=$(( guesses - 1 ))
If you are specifically using the Bash shell, then the let builtin is what you want.
The let builtin is a clean way to perform integer arithmetic in Bash. You should read the output of help let to get a better picture of how it works.
The two lines you circled in your linked picture could be written as follows:
let guesses=10 # Set "guesses" to 10
let guesses-=1 # Decrement "guesses" by 1
Most proficient Bash hackers would write these operations in this manner. You could also use the post-decrement operator.
let guesses-- # Also decrement "guesses" by 1
This may look a little cleaner to you.

How to get positive sign for result from mod in bash

When naively using the mod command in bash the residual gets the wrong sign (in my opinion) for negative numerators:
If i write:
for i in {-5..5}; do echo $(( $i % 3 )) ; done
i get the output (as a row)
-2 -1 0 -2 -1 0 1 2 0 1 2
How do i achieve the "correct" behavior
1 2 0 1 2 0 1 2 0 1 2
I know it's an old question, but rather than loop until the result is positive or launch perl or python consider the following:
for i in {-5..5}; do echo $(( (($i % 3) + 3) % 3)) ; done
this will result in the OP's desired output.
This works because the first modulo will bring the result into the range -3 to 3, adding 3, causes the result to be in the range 0 to 6, we can then perform modulo again (adding 3 has no effect on this).
in general: mod = ((a % b) + b) % b
Add 3 and then Mod 3 to the first set of results:
$ for i in {-5..5}; do printf "%d " $(( (($i % 3) + 3) % 3 )) ; done
1 2 0 1 2 0 1 2 0 1 2
If you know the maximum range, you can just add a significantly large enough multiple of 3 to make all numbers positive before the first modulo operation.
$ for i in {-5..5}; do printf "%d " $(( ($i + 3000000) % 3 )) ; done
However, the first approach is cleaner and more universal.
Lastly, for fun:
positive_mod() {
local dividend=$1
local divisor=$2
printf "%d" $(( (($dividend % $divisor) + $divisor) % $divisor ))
}
for i in {-5..5}; do
printf "%d " $(positive_mod $i 3)
done
According to wikipedia negative signs are allowed.
[The result of a mod n] this still leaves a sign ambiguity if the remainder is nonzero: two possible choices for the remainder occur, one negative and the other positive, and two possible choices for the quotient occur. Usually, in number theory, the positive remainder is always chosen, but programming languages choose depending on the language and the signs of a or n.
So it's up to the programming language to define this. As bash has obviously gone for the "negative remainder" way you might escape to e.g. perl like this:
for i in {-5..5}; do perl -le "print $i%3"; done
This is at the cost of launching the Perl interpreter individually for each integer.
Indeed! Since the OP seem to care about correct math, you might consider switching to something like python and do the looping and everything in there.

Beginner Shell, can't find the issue (array sorting)

Working on a little script which put random numbers in a 10 000 size array and then sort all this array with the method ask during the course.
I've done this code but it seem that it begin to sort (when I test I have some "a" that are printed but not as much as supposed to and I don't understand why)
I'm believing the problem come fromes my test on val array, and it's probably a beginner error but I don't really know how to find the problem on th web as I don't really now which line is the problem.
I don't necessary need an answer, just some clues to find it could be good :)
Here is my code: (new to stackoverflow so I don't know how to put a good code view directly, if anyone can show me)
for i in `seq 1 10000`;
do
val[${i}]=$RANDOM
done
echo `date +"%M.%S.%3N"`
FLAG=0
until [ $FLAG -eq 1 ]
do
FLAG=1
for j in `seq 1 9999`;
do
if [ ${val[${j}]} -gt ${val[${j+1}]} ]
then
TMP=${val[${j}]}
val[${j}]=${val[${j+1}]}
val[${j+1}]=$TMP
FLAG=0
echo a
fi
done
done
echo `date +"%M.%S.%3N"`
as asked I can't really have a useful output as I just want the date before and after the sort operation. But the sort is just supposed to put values from lower to higher by taking them two by two and invert them if necessary. Doing this until no numbers are inverted.
Edit: I tried with manual number:
10 3 6 9 1
when running it by putting echo ${val[*]} in the for loop it just print 4 times the same list in the same order, so I'm guessing it doesn't work at all... Is my use of "if" wrong ?
Edit 2: At the begining, I did it in C# and I wanted to do it in shell then, firstly because I wanted to practice shell and then because I wanted to compare efficiency and time needed for the same thing. here is the C# code, working.
Random random = new Random();
int[] _tab = new int[100000];
for (int i = 0; i < _tab.Length; i++)
{
_tab[i] = random.Next(1, _tab.Length);
}
bool perm;
int tmp;
DateTime dt = DateTime.Now;
do
{
perm = false;
for (int i = 0; i < (_tab.Length - 1); i++)
{
if (_tab[i] > _tab[i + 1])
{
tmp = _tab[i];
_tab[i] = _tab[i + 1];
_tab[i + 1] = tmp;
perm = true;
}
}
}
while (perm == true);
Console.WriteLine((DateTime.Now - dt).TotalMilliseconds);
Console.Read();
Thanks :)
If my understanding that you want to know why this script is not producing an "a" indicating the ordering of the array of the numbers initially produced in the "for" loop is correct, then here is a solution:
The syntax is incorrect for your variable expansion. The ${var} cannot have math operators inside the braces, because they have different meaning here. In a normal non-associative array Zsh handles subscripts with some basic math support, so you can use ${array[var+1]} instead of ${array[${var+1}]} as you previously did.
I suspect the reason this came about - complicated, error prone POSIX syntax - would have been avoided by using simplified Zsh syntax, but as stated in an earlier comment, it would not be portable to other shells.
Some shells support similar features: Bash supports most, but not bare subscripts ($array[var]). Strings may be ordered in Zsh in a similar manner, but the math-context brackets (( and )) would have to be replaced with normal test brackets [[ and ]] and the array $val might have to be defined with special typeset options to make the strings compare in the desired manner; that is, they might have to be padded and right or left aligned. For comparing enumeration types, like Jan - Feb, it gets a little more complicated with associative arrays and case-conversion.
Here is the script with the appropriate changes, then again in simplified Zsh:
#!/bin/sh
for i in `seq 1 10000`;
do
val[$((i))]=$RANDOM
done
echo `date +"%M.%S.%3N"`
FLAG=0
until [ $FLAG -eq 1 ]
do
FLAG=1
for j in `seq 1 9999`;
do
if [ ${val[$((j))]} -gt ${val[$((j+1))]} ]
then
TMP=${val[$((j))]}
val[$((j))]=${val[$((j+1))]}
val[$((j+1))]=$TMP
FLAG=0
echo a
fi
done
done
echo `date +"%M.%S.%3N"`
Zsh:
#!/bin/zsh
foreach i ( {1..10000} )
val[i]=$RANDOM
end
echo `date +"%M.%S.%3N"`
FLAG=0
until ((FLAG))
do
FLAG=1
foreach j ( {1..9999} )
if (( val[j] > val[j+1] ))
then
TMP=$val[j]
val[j]=$val[j+1]
val[j+1]=$TMP
FLAG=0
echo a
fi
end
done
echo `date +"%M.%S.%3N"`

Sieve of Eratosthenes UNIX Script

I'm writing a UNIX script to use the sieve to generate prime numbers. I keep getting a bad modulo division on line 19, and I can't seem to figure out why.
I have tried all kinds of different formatting, not sure what the right way is.
#!bin/bash
read -p "Upper limit? :" answer
theMultiple=2
#populate the array
for ((i=2;i<$answer;i++)); do
sieveArray[$i]=$i
done
#Use Sieve
for ((i=0;i<=${#sieveArray[*]}; i++)); do
if [ $[$(($[${sieveArray[$i]}] % $theMultiple))] -eq 0 ]; then
theMultiple=${sieveArray[$i]}
echo $theMultiple
for ((j=$i;j<${#sieveArray[*]};j++)); do
if [ $[$(($[${sieveArray[$j]}] % $theMultiple))] -eq 0 ]; then
sieveArray[$j]=0
fi
done
fi
done
}
You start filling in your sieveArray at index 2, yet in your main loop you start using it at index zero. The first two elements are probably set to zero by default which causes a division by zero.
You could write it differently with less bash arithmetic and more commands :
#!/bin/bash
limit=$1
sieve="$(seq 2 $limit|sort)"
for n in 2 $(seq 3 2 $limit)
do
sieve="$(comm -23 <(echo "$sieve") <(seq $(($n * $n)) $n $limit|sort))"
done
echo "$sieve"|sort -n
seq is used to generate lists of numbers and multiples.
comm is used to remove the multiples from the sieve variable. As comm expects data sorted un alphabetical order (10 is before 9), the lists of number must be sorted every time.
The for loop is slightly optimized to not include even numbers, except 2.

Shell script random number generating

var=$RANDOM creates random numbers but how can i specify a range like between 0 and 12 for instance?
If you already have your random number, you can say
var=$RANDOM
var=$[ $var % 13 ]
to get numbers from 0..12.
Edit:
If you want to produce numbers from $x to $y, you can easily modify this:
var=$[ $x + $var % ($y + 1 - $x) ]
Between 0 and 12 (included):
echo $((RANDOM % 13))
Edit: Note that this method is not strictly correct. Because 32768 is not a multiple of 13, the odds for 0 to 8 to be generated are slightly higher (0.04%) than the remaining numbers (9 to 12).
Here is shell function that should give a balanced output:
randomNumber()
{
top=32768-$((32768%($1+1)))
while true; do
r=$RANDOM
[ r -lt $top ] && break
done
echo $((r%$1))
}
Of course, something better should be designed if the higher value of the range exceed 32767.
An alternative using shuf available on linux (or coreutils to be exact):
var=$(shuf -i0-12 -n1)
Here you go
echo $(( $RANDOM % 12 ))
I hope this helps.
This document has some examples of using this like RANGE and FLOOR that might be helpful: http://tldp.org/LDP/abs/html/randomvar.html
On FreeBSD and possibly other BSDs you can use:
jot -r 3 0 12
This will create 3 random numbers from 0 to 12 inclusively.
Another option, if you only need a single random number per script, you can do:
var=$(( $$ % 13 ))
This will use the PID of the script as the seed, which should be mostly random. The range again will be from 0 to 12.

Resources