How to add 5 to every number of an array with Bash? - bash

I am trying to create an array of user inputs and then add to each element in the array:
read number
for i in 1 2 3
read array[$i]
done
let position=0
for i in "${array[#]}"
do
let array[position]+=($i+$number)
let "position++"
done
for (( i=0; $i<3; i=$1+1 ))
do
echo ${array[$1]}
So, the user will enter "5" for number and then three more numbers for the array(90, 80, 70). The results should be array(95, 85, 75), but the output I'm getting is array(95, 175, 155).

A saner way to write this would be:
read -r number
read -r -a array
for idx in "${!array[#]}"; do
(( array[$idx] += number ))
done
printf '%s\n' "${array[#]}"
Instead of assuming that the indexes start at 0 (which they didn't, originally, because you were explicitly assigning to positions 1, 2 and 3), using "${!array[#]}" finds the actual indexes, and thus works correctly even with sparse arrays or ones not indexed starting at position 0.
Instead of duplicating $i (aka the values from your array) on the right-hand side of +=, it only adds the number itself.
Instead of iterating over indexes (again) to print the values, it just asks the array to dump all its values in index order with "${array[#]}".
See this in operation at https://ideone.com/WTLJSu
There is a behavioral difference insofar as it expects all the array values to be passed on a single line of input. If you don't want that, see the version at https://ideone.com/3OQtt3 instead.

Related

How to avoid line insert to the file if the line is already present in the file?

How should the check be made so that there are no line duplicates in the file
open ( FILE, ">newfile");
for( $a = 1; $a < 20; $a = $a + 1 ) {
my $random_number = 1+ int rand(10);;
# check to avoid inserting the line if the line is already present in the file
print FILE "Random number is $random_number \n";
}
close(FILE);
!$seen{$_}++ is a common idiom for identifying duplicates.
my %seen;
for (1..19) {
my $random_number = 1+ int rand(10);
say "Random number is $random_number" if !$seen{$random_number}++;
}
But that doesn't guarantee that you will get all numbers from 1 to 10 in random order. If that's what you are trying to achieve, the following is a far better solution:
use List::Util qw( shuffle );
say "Random number is $_" for shuffle 1..10;
It seems like what you are asking is how to randomize the order of the numbers 1 to 20. I.e. no duplicates, random order. That can be easily done with a Schwartzian transform. For example:
perl -le'print for map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [$_, rand()] } 1..20'
6
7
16
14
5
20
3
13
19
17
4
8
15
10
9
11
18
1
2
12
In this case, reading from the end and backwards, we create a list of numbers 1 .. 20, we feed that into a map statement which turns each number into an array-ref, containing the number, and a random number. Then we feed that list of array refs to a sort, where we sort numerically on the second argument in the array ref: the random number (hence creating a random order). Then we transform the array ref back into a simple number with another map statement. Finally we print the list using a for loop.
So in your case, the code would look something like:
print "Random number is: $_\n" for # print each number
map { $_>[0] } # restore to a number
sort { $a->[1] <=> $b->[1] } # sort the list on the random number
map { [ $_, rand() ] } # create array ref with random number as index
1 .. 20; # create list of numbers to randomize order of
Then you can use the program like below to redirect output to a file:
$ perl numbers.pl > newfile.txt
Enter each line into a hash as well, what makes it easy and efficient to later check for it
use warnings;
use strict;
use feature 'say';
my $filename = shift or die "Usage: $0 filename\n";
open my $fh, '>', $filename or die "Can't open $filename: $!";
my %existing_lines;
for my $i (1..19)
{
my $random_number = 1 + int rand(10);
# Check to avoid inserting the line if it is already in the file
if (not exists $existing_lines{$random_number}) {
say $fh "Random number is $random_number";
$existing_lines{$random_number} = 1;
}
}
close $fh;
This assumes that the intent in the question is to not repeat that number (symbolizing content to be stored without repetition).
But if it is indeed the whole line (sentence) to be avoided, where that random number is used merely to make each line different, then use the whole line for the key
for my $i (1..19)
{
my $random_number = 1 + int rand(10);
my $line = "Random number is $random_number";
# Check to avoid inserting the line if it is already in the file
if (not exists $existing_lines{$line}) {
say $fh $line;
$existing_lines{$line} = 1;
}
}
Notes and literature
Lexical filehandles (my $fh) are much better than globs (FILE), and the three-argument open is better. See the quide perlopentut and reference open
Always check the open call (or die... above). It can and does fail -- quietly. In that check always print the error for which it failed, $!
The C-style for loop is very rarely needed while the usual foreach (with synonym for) is much nicer to use; see it in perlsyn. The .. is the range operator
Always declare variables with my, and enforce that with strict pragma; always use warnings
If the filehandle refers to pipe-open (not the case here) always check its close
See perlintro for a general overview and for hashes; for more about Perl's data types see perldata. Keep in mind for later the notion of complex data structures, perldsc
return false will do the trick.
Because you cannot generate 20 distinct numbers in the range [1, 10].

Looping over associative array groups

I have an associative array asc with its keys in the following form
asc[1-0]=dlc[0]
asc[2-1]=dlc[1]
asc[3-2]=dlc[2]
asc[1-3]=dlc[3]
asc[2-4]=dlc[4]
asc[3-5]=dlc[5]
asc[1-6]=dlc[6]
asc[2-7]=dlc[7]
asc[3-8]=dlc[8]
asc[1-9]=dlc[9]
asc[2-10]=dlc[10]
asc[3-11]=dlc[11]
asc[1-12]=dlc[12]
asc[2-13]=dlc[13]
...
I would like to group the elements by the first number when I call a function fn.
loop over i
fn asc[i-*] $ pass all elements with i as first number
You'll have to iterate over the keys of the associative array and build up an ordinary array of the values associated with the desired subset of keys.
for i in 1 2 3; do
group=()
for k in "${!asc[#]}"; do
[[ $k = $i-* ]] || continue
group+=("${asc[$k]}")
done
fn "${group[#]}"
done

How do I get the unique values of a list in bash preserving order and keep the last value for each unique?

I have a list in bash that can have repeated values in it. I would like to remove duplicates and get a list with only the unique values in it. Order must be preserved and the last occurrence of the unique values is the one I wish to keep.
For example, if I have this list:
A=( D B A C D )
I'm looking for this:
result=( B A C D )
I've seen solutions for this when the data is a list in a file, but I'd prefer to keep the list in-memory without jumping through any hoops.
I think I can use an associative array and loop through the list adding the entries as keys in the array and then just dump the keys into the unique list but I'm not an expert with associative arrays across platforms -- do they sort themselves on key value sort of like a lot of C++ STL containers do or do they preserve the order of insertion regardless of key values?
I'd like to avoid a reliance on associative arrays though, because not all systems I may need to run on have bash 4.x or higher... some will be bash 3.x...
Any help would be great.
Without Associative Arrays
You can do it with indexed arrays by using an intermediate indexed array to hold unique values from A. This requires a nested loop over values stored in c[] for each element of A, e.g.
#!/bin/bash
declare -a result # declare result indexed array
declare -a c # declare temp intermediate indexed array
A=( D B A C D ) # original with duplicates
## loop decending over A, reset found flag, loop over c, if present continue,
# otherwise store A at index in c
for ((i = $((${#A[#]}-1)); i >= 0; i--)); do
found=0;
for j in ${c[#]}; do
[ "$j" = "${A[i]}" ] && { found=1; break; }
done
[ "$found" -eq '1' ] && continue
c[i]=${A[i]}
done
## loop over c testing if index for A exists, add from c to result
for ((i = 0; i < ${#A[#]}; i++)); do
[ "${c[i]}" ] && result+=(${c[i]})
done
declare -p result # output result
Example Use/Output
$ bash lastuniqindexed.sh
declare -a result='([0]="B" [1]="A" [2]="C" [3]="D")'
Using Associative Arrays with BASH_VERSION Test
You can do it with a combination of indexed and associative arrays making only a single pass though each array. You use an associative array B keyed with the value of A using B as a frequency array indicating whether an element of A has been seen. You then store the element of A in a temporary indexed array c[] so that the unique values can be added to result preserving the original order.
You can address whether associative array functionality is present with a bash version test at the beginning, e.g.
#!/bin/bash
case $BASH_VERSION in
## empty or beginning with 1, 2, 3
''|[123].*) echo "ERROR: Bash 4.0 needed" >&2
exit 1;;
esac
declare -A B # declare associative array
declare -a result # declare indexed array
A=( D B A C D ) # original with duplicates
## loop decending over A, if B[A] doesn't exist, set B[A]=1, store in c[]
for ((i = $((${#A[#]}-1)); i >= 0; i--)); do
[ -n "${B[${A[i]}]}" ] || { B[${A[i]}]=1; c[i]=${A[i]};}
done
## loop over c testing if index for A exists, add from c to result
for ((i = 0; i < ${#A[#]}; i++)); do
[ "${c[i]}" ] && result+=(${c[i]})
done
declare -p result # output result
Without the use of associative arrays, the nested loops looping over the original checking against each entry in c[] will be much less efficient as the size of the array grows.
Example Use/Output
$ bash lastuniq.sh
declare -a result='([0]="B" [1]="A" [2]="C" [3]="D")'
Look things over and let me know if you have further questions.

Remove multiple elements from array based on index

I would like to remove multiple elements from an array based on their indexes.
array=("a" "b" "c" "d")
indexes=(1 3)
Output should be
array=("a" "c")
I know how to remove an element from an array knowing the index of the element:
If $i is the index:
array=("${(#)array[1,$i-1]}" "${(#)array[$i+1,$#array]}")
But what if I have multiple elements to remove? If I loop over the array of indexes, once I have removed one element the other indexes won't correspond anymore to the elements to be removed. So how is it possible to do that?
Using BASH arrays you can do this easily:
# original array
array=("a" "b" "c" "d")
# indexes array
indexes=(1 3)
# loop through indexes array and delete from element array
for i in "${indexes[#]}"; do
unset "array[$i]"
done
# check content of original array
declare -p array
declare -a array=([0]="a" [2]="c")
As per Chepner's comments below if OP wants an array of contiguous indices then loop through differential array and populate a new array
# result array
out=()
# loop through differential array and populate result
for i in "${array[#]}"; do
out+=("$i")
done
declare -p out
declare -a out=([0]="a" [1]="c")
Assuming indices is sorted, keep a counter of how many items you have already removed, and subtract that from each index in your loop.
count=0
for i in $indices; do
c=$((i - count))
array=("${(#)array[1,$c-1]}" "${(#)array[$c+1,$#array]}")
count=$((count + 1))
done
In bash, the same approach looks like
count=0
for i in "${indices[#]}"; do
c=$((i - count))
array=( "${array[#]:0:c-1}" "${array[#]:c+1}" )
count=$((count + 1))
done

'for' loop with dynamic array size

I have an array that gets elements added to it when it calls the function findVar. The problem seems to be on the for loop that does not update the number of elements once started running.
When I do echo at the end of the for loop I get the correct number of elements and the last element but it seems not to be checking on the for conditions once started.
for var in "${tempV[#]}"
do
num_words=${#tempV[#]}
let i=i+1
if ! [ $i -gt $num_words ]
then
findVar $objBKP $var
fi
done
Your attempt is looping over every original element in the array and then ensuring that you haven't looped more times than that and calling your function.
That doesn't work because the original expansion of tempV happens once and so the added entries are never seen. But that also doesn't make sense since, by definition, if you are looping over the elements of the array you can't loop more times then there are elements in the array.
What you want to be doing (assuming a non-sparse, integer-indexed array that is only appended to) is looping numerically and checking that you haven't exceeded the array size as the loop condition.
Something like this (untested):
i=0
while [ "$i" -lt "${#tempV[#]}" ]; do
var=${tempV[i]}
findVar "$objBKP" "$var"
i=$((i + 1))
done
You're not using $i for anything other than an iteration counter. It's completely unnecessary in your posted example. Instead, just iterate over the contents of the variable expansion. For example:
for var in "${tempV[#]}"; do
findVar "$objBKP" "$var"
done

Resources