I am using Hacker Rank challenges to teach myself BASH, and I'm in need of some advice.
I'm specifically trying to solve this challenge: Apple and Oranges by nabila_ahmed
I need to read in multiple lines of ints separated by spaces, on multiple lines. I decided to use awk to do this because it seems a lot more efficient in memory storage than using read. (I tried a couple of solutions using read and they timed out, because the test cases are really big.)
Example input:
7 11
5 15
3 2
-2 2 1
5 -6
This is my first attempt in bash and it timed out:
row=0
while read line || [[ -n $line ]]; do
if [ "$row" -eq 0 ]
then
column=0
for n in $line; do
if [ "$column" -eq 0 ]
then
housePos1=$n
elif [ "$column" -eq 1 ]
then
housePos2=$n
fi
((column++))
done
# Calculate house min and max
if [ "$housePos1" -gt "$housePos2" ]
then
minHousePos=$housePos2
maxHousePos=$housePos1
else
minHousePos=$housePos1
maxHousePos=$housePos2
fi
elif [ "$row" -eq 1 ]
then
column=0
for n in $line; do
if [ "$column" -eq 0 ]
then
appleTreePos=$n
elif [ "$column" -eq 1 ]
then
orangeTreePos=$n
fi
((column++))
done
elif [ "$row" -eq 3 ]
then
applesInHouse=0
for n in $line; do
# Calculate the apple's position
let applePos=$((appleTreePos + n))
# If the apple's position is within the houses position, count it
if [ "$applePos" -ge "$minHousePos" ] && [ "$applePos" -le "$maxHousePos" ]
then
((applesInHouse++))
fi
done
elif [ "$row" -eq 4 ]
then
orangesInHouse=0
for n in $line; do
# Calculate the apple's position
let orangePos=$((orangeTreePos + n))
# If the apple's position is within the houses position, count it
if [ "$orangePos" -ge "$minHousePos" ] && [ "$orangePos" -le "$maxHousePos" ]
then
((orangesInHouse++))
fi
done
fi
((row++))
done
echo "$applesInHouse"
echo "$orangesInHouse"
Here is my second attempt in bash, even more of the solutions timed out:
x=0;y=0;read -r s t;read -r a b;read -r m n;
for i in `seq 1 $m`; do
if [ "$i" -lt "$m" ]
then
read -d\ z
else
read -r z
fi
if [ "$((a+z))" -ge "$s" ] && \
[ "$((a+z))" -le "$t" ]
then
((x++))
fi
done
for i in `seq 1 $n`; do
if [ "$i" -lt "$n" ]
then
read -d\ z
else
read -r z
fi
if [ "$((b+z))" -ge "$s" ] && \
[ "$((b+z))" -le "$t" ]
then
((y++))
fi
done
echo $x; echo $y
Here's where I am at in debugging my solution using awk...
awk -v RS='[-]?[0-9]+' \
'{
if(word==$1) {
counter++
if(counter==1){
s=RT
}else if(counter==2){
t=RT
}else if(counter==3){
a=RT
}else if(counter==4){
b=RT
}else if(counter==5){
m=RT
}else if(counter==6){
n=RT
}else{
counter2++
if(counter2<=m){
print "Apples:"
print a+RT
print a+RT>=s
print a+RT<=t
applecount++
}
if(counter2>m && counter2<=m+n){
print "Oranges:"
print b+RT
print b+RT>=s
print b+RT<=t
orangecount++
}
}
}else{
counter=1
word=$1
}
}
END {
print "Total Counts:"
print applecount
print orangecount
}
'
Here is the output from that script when using the sample input
Apples:
3
0
0
Apples:
7
1
0 <-- This is the problem! (7 is less than or equal to 11)
Apples:
6
0
0
Oranges:
20
0
0
Oranges:
9
1
0 <-- This is also a problem! (9 is less than or equal to 11)
Total Counts:
3
2
As you can see, I'm getting some of the wrong comparisons...
ANSWER
(mostly courtesy of #glenn-jackman)
apples_oranges() {
local s t a b m n d
local -a apples oranges
local na=0 nb=0
{
read s t
read a b
read m n
read -a apples
read -a oranges
} < "$1"
for d in "${apples[#]}"; do
(( s <= a+d && a+d <= t )) && ((na++))
done
echo $na
for d in "${oranges[#]}"; do
(( s <= b+d && b+d <= t )) && ((nb++))
done
echo $nb
}
apples_oranges /dev/stdin
I'd do this with bash
apples_oranges() {
local s t a b m n d
local -a apples oranges
local na=0 nb=0
{
read s t
read a b
read m n # unused
read -a apples
read -a oranges
} < "$1"
for d in "${apples[#]}"; do
(( a+d >= s )) && ((na++))
done
echo $na
for d in "${oranges[#]}"; do
(( b-d <= t )) && ((nb++))
done
echo $nb
}
apples_oranges input.txt
this may get you started...
$ awk '
NR==1{split($0,house)}
NR==2{split($0,trees)}
NR==3{split($0,counts)}
NR==4{split($0,apples)}
NR==5{split($0,oranges)}
END{for(i in apples)
if(trees[1]+apples[i]>=house[1] && trees[1]+apples[i]<=house[2]) a++; print a}' file
Related
Im trying to get an array from grades.txt, and determine what letter grade it should be assigned.
I either get
hw4part2.sh: line 26: [: : integer expression expected
If i use -ge or
hw4part2.sh: line 26: [: : unary operator expected
If i use >=
Below is the code im trying to get working
mapfile -t scores < grades.txt
numOScores=0
numOA=0
numOB=0
numOC=0
numOD=0
numOF=0
DoneWScores=0
A=90
B=80
C=70
D=60
F=59
while [ $DoneWScores -eq 0 ]
do
numOScores=$((numOScores + 1))
if [ "${scores[$numOScores]}" -ge "$A" ]
then
echo "A"
elif [ "${scores[$numOScores]}" -ge "$B" ]
then
echo "B"
elif [ "${scores[$numOScores]}" -ge "$C" ]
then
echo "C"
elif [ "${scores[$numOScores]}" -ge "$D" ]
then
echo "D"
elif [ "${scores[$numOScores]}" -le "$F" ]
then
echo "F"
else
echo "Done/error"
DoneWScores=1
fi
done
If anyone knows what my problem is, that'd be greatly appreciated
Consider this:
#!/usr/bin/env bash
if (( ${BASH_VERSINFO[0]} < 4 )); then
echo "Bash version 4+ is required. This is $BASH_VERSION" >&2
exit 1
fi
letterGrade() {
if (( $1 >= 90 )); then echo A
elif (( $1 >= 80 )); then echo B
elif (( $1 >= 70 )); then echo C
elif (( $1 >= 60 )); then echo D
else echo F
fi
}
declare -A num
while read -r score; do
if [[ $score == +([[:digit:]]) ]]; then
grade=$(letterGrade "$score")
(( num[$grade]++ ))
echo "$grade"
else
printf "invalid score: %q\n" "$score"
fi
done < grades.txt
for grade in "${!num[#]}"; do
echo "$grade: ${num[$grade]}"
done | sort
I'm implementing a merge sort algorithm in bash, but looks like it loops forever and gives error on m1 and m2 subarrays. It's a bit hard to stop loop in conditions since I have to use echo and not return. Anyone have any idea why this happens?
MergeSort (){
local a=("$#")
if [ ${#a[#]} -eq 1 ]
then
echo ${a[#]}
elif [ ${#a[#]} -eq 2 ]
then
if [ ${a[0]} -gt ${a[1]} ]
then
local t=(${a[0]} ${a[1]})
echo ${t[#]}
else
echo ${a[#]}
fi
else
local p=($(( ${#a[#]} / 2 )))
local m1=$(MergeSort "${a[#]::p}")
local m2=$(MergeSort "${a[#]:p}")
local ret=()
while true
do
if [ "${#m1[#]}" > 0 ] && [ "${#m2[#]}" > 0 ]
then
if [ ${m1[0]} <= ${m2[0]} ]
then
ret+=(${m1[0]})
m1=${m1[#]:1}
else
ret+=(${m2[0]})
m2=${m2[#]:1}
fi
elif [ ${#m1[#]} > 0 ]
then
ret+=(${ret[#]} ${m1[#]})
unset m1
elif [ ${#m2[#]} > 0 ]
then
ret+=(${ret[#]} ${m2[#]})
unset m2
else
break
fi
done
fi
echo ${ret[#]}
}
a=(6 5 6 4 2)
b=$(MergeSort "${a[#]}")
echo ${b[#]}
There are multiple issues in your shell script:
you should use -gt instead of > for numeric comparisons on array lengths
<= is not a supported string comparison operator. You should use < and quote it as '<', or better use '>' and transpose actions to preserve sort stability.
there is no need for local t, and your code does not swap the arguments. Just use echo ${a[1]} ${a[0]}
you must parse the result of recursive calls to MergeSort as arrays: local m1=($(MergeSort "${a[#]::p}"))
when popping initial elements from m1 and m2, you must reparse as arrays: m1=(${m1[#]:1})
instead of ret+=(${ret[#]} ${m1[#]}) you should just append the elements with ret+=(${m1[#]}) and instead of unset m1, you should break from the loop. As a matter of fact, if either array is empty you should just append the remaining elements from both arrays and break.
furthermore, the while true loop should be simplified as a while [ ${#m1[#]} -gt 0 ] && [ ${#m2[#]} -gt 0 ] loop followed by the tail handling.
the final echo ${ret[#]} should be moved inside the else branch of the last if
to handle embedded spaces, you should stringize all expansions but as the resulting array is expanded with echo embedded spaces that appear in the output are indistinguishable from word breaks. There is no easy workaround for this limitation.
Here is a modified version:
#!/bin/bash
MergeSort (){
local a=("$#")
if [ ${#a[#]} -eq 1 ]; then
echo "${a[#]}"
elif [ ${#a[#]} -eq 2 ]; then
if [ "${a[0]}" '>' "${a[1]}" ]; then
echo "${a[1]}" "${a[0]}"
else
echo "${a[#]}"
fi
else
local p=($(( ${#a[#]} / 2 )))
local m1=($(MergeSort "${a[#]::p}"))
local m2=($(MergeSort "${a[#]:p}"))
local ret=()
while [ ${#m1[#]} -gt 0 ] && [ ${#m2[#]} -gt 0 ]; do
if [ "${m1[0]}" '>' "${m2[0]}" ]; then
ret+=("${m2[0]}")
m2=("${m2[#]:1}")
else
ret+=("${m1[0]}")
m1=("${m1[#]:1}")
fi
done
echo "${ret[#]}" "${m1[#]}" "${m2[#]}"
fi
}
a=(6 5 6 4 2 a c b c aa 00 0 000)
b=($(MergeSort "${a[#]}"))
echo "${b[#]}"
Output: 0 00 000 2 4 5 6 6 a aa b c c
The challenge is to sum the digits of a given number till the result has only one digit. Let say the number is "999" (9+9+9=27, 2+7=9). This is what I did till now.
#!/bin/bash
set +m
shopt -s lastpipe
NUMBER=999
DIGITS=`echo "${#NUMBER}"`
FINALSUM=0
if [ "$DIGITS" -gt 0 ] && [ "$DIGITS" -gt 1 ]; then
grep -o . <<< "${NUMBER}" | while read DIGIT; do
declare -x FINALSUM="$(($FINALSUM+$DIGIT))"
done
echo $FINALSUM
else
echo $SOMA
fi
A bit slow for large numbers:
function sumit {
i="$1"
while [ "$i" -gt 10 ]; do
(( i=i%10 + i/10 ))
done
echo "$1 => $i"
}
# Test
for i in 10 15 999 222 2229; do
sumit $i
done
Can an awk-ward guy join in?
$ awk -v i=999 '
BEGIN {
while( split(i,a,"") > 1) {
i=0;
for( j in a ) i+=a[j]
}
print i
}'
9
I have a set of valid characters [0-9a-z_] and a variable that is assigned one of these characters. What I want to do is to be able to increment that variable to the next in the set.
If need be I can handle the "special" cases where it would increment from '9' to 'a' and 'z' to '_', but I can't figure out how to increment letters.
#!/bin/bash
y=b
echo $y # this shows 'b'
y=$((y+1))
echo $y # this shows '1', but I want it to be 'c'
y=b
echo "$y" # this shows 'b'
y=$(echo "$y" | tr "0-9a-z" "1-9a-z_")
echo "$y" # this shows 'c'
Note that this does not handle the case where $y = "_" (not sure what you want then, and in any case it'll probably require separate handling), and if $y is more than one character long it'll "increment" all of them (i.e. "10" -> "21", "09g" -> "1ah", etc).
Maybe this can be a solution:
a=({0..9} {a..z} _)
echo ${a[*]}
yc=11
echo ${a[yc]}
((++yc))
echo ${a[yc]}
echo ${a[++yc]}
#Alternative
declare -A h
# Fill the has point to the next character
for((i=0;((i+1))<${#a[*]};++i)) { h[${a[i]}]=${a[i+1]};}
y=b
echo $y, ${h[$y]}
Output:
0 1 2 3 4 5 6 7 8 9 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 _
b
c
d
b, c
for those who would like to print incremented letter by execution of a function:
ALPHA=( {A..Z} )
alpha_increment () { echo ${ALPHA[${i:-0}]}; ((i++)) ;}
alpha_increment
A
alpha_increment
B
alpha_increment
C
You can start with this:
echo 0x$(( $(printf "%x" "'b'") + 1)) | xxd -r
I wrote this for a project, it uses the chr and ord fucntions(also found here somewhere) and some pure bash(only external called in the function is tr), if you are doing more than 100 characters I would use something else, but for short strings in my testing its actually slightly faster than python.
Also this script lower cases any input, you will have to modify it for upper case.
after putting these functions in your script(or cut and paste into a shell) you can just do
inc_string abz9z9
and get back.
aca0a0
chr() {
[ "$1" -lt 256 ] || return 1
printf "\\$(printf '%03o' "$1")"
}
ord() {
LC_CTYPE=C printf '%d' "'$1"
}
inc_string ()
{
string="$1";
lcstring=$(echo $string | tr '[:upper:]' '[:lower:]');
for ((position=$((${#lcstring}-1));position>=0;position--));do
if [ "${lcstring:position:1}" = 'z' ]; then
if [ "$position" -eq "$((${#lcstring}-1))" ]; then
newstring="${lcstring:0:$(($position))}a";
lcstring="$newstring";
elif [ "$position" -eq "0" ]; then
newstring="a${lcstring:$((position+1))}";
echo $newstring;
break;
else
newstring="${lcstring:0:$(($position))}a${lcstring:$((position+1))}";
lcstring="$newstring";
fi
elif [ "${lcstring:position:1}" = '9' ]; then
if [ "$position" -eq "$((${#lcstring}-1))" ]; then
newstring="${lcstring:0:$(($position))}0";
lcstring="$newstring";
elif [ "$position" -eq "0" ]; then
newstring="0${lcstring:$((position+1))}";
echo $newstring;
break;
else
newstring="${lcstring:0:$(($position))}0${lcstring:$((position+1))}";
lcstring="$newstring";
fi
else
if [ "$position" -eq "$((${#lcstring}-1))" ]; then
newstring="${lcstring:0:$(($position))}$(chr $(($(ord ${lcstring:position})+1)))";
echo $newstring;
break;
elif [ "$position" -eq "0" ]; then
newstring="$(chr $(($(ord ${lcstring:position})+1)))${lcstring:$((position+1))}";
echo $newstring;
break;
else
newstring="${lcstring:0:$(($position))}$(chr $(($(ord ${lcstring:position})+1)))${lcstring:$(($position+1))}";
echo $newstring;
break;
fi
fi
done
}
My current script does the following;
It takes integer as a command line argument and starts from 1 to N , it checks whether the numbers are divisible by 3, 5 or both of them. It simply prints out Uc for 3, Bes for 5 and UcBes for 3,5. If the command line argument is empty, it does the same operation but the loop goes to 1 to 20.
I am having this error "Too many arguments at line 11,15 and 19".
Here is the code:
#!/bin/bash
if [ ! -z $1 ]; then
for i in `seq 1 $1`
do
if [ [$i % 3] -eq 0 ]; then
echo "Uc"
elif [ i % 5 -eq 0 ]; then
echo "Bes"
elif [ i % 3 -eq 0 ] && [ i % 5 -eq 0 ]
then
echo "UcBes"
else
echo "$i"
fi
done
elif [ -z $1 ]
then
for i in {1..20}
do
if [ i % 3 -eq 0 ]
then
echo "Uc"
elif [ i % 5 -eq 0 ]
then
echo "Bes"
elif [ i % 3 -eq 0 ] && [ i % 5 -eq 0 ]
then
echo "UcBes"
else
echo "$i"
fi
done
else
echo "heheheh"
fi
Note that [ is actually synonym for the test builtin in shell (try which [ in your terminal), and not a conditional syntax like other languages, so you cannot do:
if [ [$i % 3] -eq 0 ]; then
Moreover, always make sure that there is at least one space between [, ], and the variables that comprise the logical condition check in between them.
The syntax for evaluating an expression such as modulo is enclosure by $((...)), and the variable names inside need not be prefixed by $:
remainder=$((i % 3))
if [ $remainder -eq 0 ]; then
You should probably use something like :
if [ $(($i % 3)) -eq 0 ]
instead of
if [ $i % 3 -eq 0 ]
if [ [$i % 3] -eq 0 ]
Your script could be greatly simplified. For example:
#!/bin/sh
n=0
while test $(( ++n )) -le ${1:-20}; do
t=$n
expr $n % 3 > /dev/null || { printf Uc; t=; }
expr $n % 5 > /dev/null || { printf Bes; t=; }
echo $t
done
gives slightly different error messages if the argument is not an integer, but otherwise behaves the same.