12

I have a matrix that looks like following:

Input:

A   B   C   D   E   F   G   H   I 
0   0   0   0   1   0   0   0   1
0   0   0   1   0   0   0   0   0  
0   0   0   1   0   0   0   0   0  
1   0   0   0   0   0   0   0   0  
1   0   1   0   0   0   1   0   0  
1   0   0   1   0   0   0   1   0  
1   0   0   0   1   1   1   0   0  

And I would like to extract for each row the list of letter corresponding to the value 1.

Output:

E,I 
D
D
A
A,C,G  
A,D,H  
A,E,F,G  

I have tried to split the header and to match the words with numbers but I failed.

12

In awk:

NR == 1 { for(column=1; column <= NF; column++) values[column]=$column; }
NR > 1 { output=""
        for(column=1; column <= NF; column++)
                if($column) output=output ? output "," values[column] : values[column]
        print output }
  • 6
    can also use NR == 1 { split($0,values) } – Sundeep Oct 11 '17 at 15:54
  • That's skipping the 2nd line. Consider putting a next at the end of the first line so you don't need to test an opposite condition for subsequent lines. – Ed Morton Oct 11 '17 at 16:11
  • 1
    Appears the original input text had an extra blank line in it, which I coded for. It's since been edited out, so just change NR > 2 to NR > 1. – Jeff Schaller Oct 11 '17 at 16:12
  • 1
    Thank you for the "golfing" tip, Sundeep! I think I prefer the explicit 'for' loop as it lines up visually/logically with the 'for' loop in the body. – Jeff Schaller Oct 11 '17 at 16:16
  • 1
    @fusion.slope, either pass the entire code in a single-quoted argument to awk, or paste the code into a file and run it with awk -f that.script.file input-file – Jeff Schaller Oct 11 '17 at 16:17
6

Another one with perl

$ perl -lane 'if($. == 1){ @h=@F }
              else{@i = grep {$F[$_]==1} (0..$#F); print join ",",@h[@i]}
             ' ip.txt
E,I
D
D
A
A,C,G
A,D,H
A,E,F,G
  • -a option to split input line on white-spaces, available in @F array
  • if($. == 1){ @h=@F } save the header if first line
  • @i = grep {$F[$_]==1} (0..$#F) save index if entry is 1
  • print join ",",@h[@i] print only those index from header array using , as separator
4

Still for the fun of it, a zsh version:

{
   read -A a  &&
   while read -A b; do
     echo ${(j<,>)${(s<>)${(j<>)a:^b}//(?0|1)}}
   done
} < file
  • ${a:^b} zips the two arrays, so you get A 0 B 0 C 0 D 0 E 1 F 0 G 0 H 0 I 1
  • ${(j<>)...} joins the elements with nothing in between so it becomes A0B0C0D0E1F0G0H0I1
  • ${...//(?0|1)} we strip the ?0 and 1 from it so it becomes EI:
  • ${(s<>)...} split on nothing to get an array of one element per letter: E I
  • ${(j<,>)...} join those with , -> E,I.
  • this is just a simple bash right? – fusion.slope Oct 12 '17 at 13:13
  • 1
    @fusion.slope, No, that's zsh, a different shell from bash (and much more powerful, and with a much better design if you ask me). bash has borrowed only a tiny fraction of zsh's feature (like {1..4}, <<<, **/*) not the ones mentioned here, Most of bash's features are otherwise borrowed from ksh. – Stéphane Chazelas Oct 12 '17 at 13:34
3

Another awk solution:

awk 'NR==1{ split($0,a); next }   # capture and print `header` fields
     { for (i=1;i<=NF;i++)         # iterating through value fields `[0 1 ...]`
           if ($i) { printf "%s",(f?","a[i]:a[i]); f=1 } 
       f=0; print "" 
     }' file

The output:

E,I
D
D
A
A,C,G
A,D,H
A,E,F,G
2

Here is a solution in Perl:

use strict;

my @header = split /\s+/, <>;
<>; ## Skip blank line
while (<>) {
    my @flags = split /\s+/;
    my @letters = ();
    for my $i (0 .. scalar @flags - 1) {
        push @letters, $header[$i] if $flags[$i];
    }

    print join(',', @letters), "\n";
}

It works by reading the header columns into an array and then, for each data row, copying the column name to an output array if the matching data column evaluates as true. The column names are then printed comma-separated.

2

A sed one for the fun of it:

sed '
  s/ //g
  1{h;d;}
  G;s/^/\
/
  :1
    s/\n0\(.*\n\)./\
\1/
    s/\n1\(.*\n\)\(.\)/\2\
\1/
  t1
  s/\n.*//
  s/./&,/g;s/,$//'

With GNU sed, you can make it a bit more legible with:

sed -E '
  s/ //g # strip the spaces

  1{h;d} # hold the first line

  G;s/^/\n/ # append the held line and prepend an empty line so the
            # pattern space becomes <NL>010101010<NL>ABCDEFGHI we will
            # build the translated version in the part before the first NL
            # eating one character at a time off the start of the
            # 010101010 and ABCDEFGHI parts in a loop:
  :1
    s/\n0(.*\n)./\n\1/     # ...<NL>0...<NL>CDEFGHI becomes
                           # ...<NL>...<NL>DEFGHI (0 gone along with C)

    s/\n1(.*\n)(.)/\2\n\1/ # ...<NL>1...<NL>CDEFGHI becomes
                           # ...C<NL>...<NL>DEFGHI (1 gone but C moved to 
                           #                        the translated part)
  t1 # loop as long as any of those s commands succeed

  s/\n.*// # in the end we have "ADG<NL><NL>", strip those NLs

  s/./,&/2g # insert a , before the 2nd and following characters'

A slightly shorter version, assuming there's always the same number of digits on each line:

sed -E '
  s/ //g
  1{H;d}
  G
  :1
    s/^0(.*\n)./\1/
    s/^1(.*\n)(.*\n)(.)/\1\3\2/
  t1
  s/\n//g
  s/./,&/2g'

Same as above except we're swapping the translated and index parts which allows for some optimisations.

  • if you can explain would be good for the community. Thanks in advance – fusion.slope Oct 12 '17 at 11:51
  • 1
    @fusion.slope, see edit. – Stéphane Chazelas Oct 12 '17 at 12:00
  • nice the loop with t1 command! – fusion.slope Oct 12 '17 at 12:05
1

python3

python3 -c '
import sys
header = next(sys.stdin).rstrip().split()
for line in sys.stdin:
  print(*(h*int(f) for (h, f) in zip(header, line.rstrip().split()) if int(f)), sep=",")

  ' <file
E,I
D
D
A
A,C,G
A,D,H
A,E,F,G
0

Pure bash solution:

read -a h
while read -a r
do (
    for i in ${!r[@]}
    do 
        (( r[i] == 1 )) && y[i]=${h[i]}
    done
    IFS=,
    echo "${y[*]}")
done
  • 3
    Please explain how this solves the problem. – Scott Oct 12 '17 at 3:22
  • That is left as an exercise for the reader. Assuming basic bash knowledge LESS="+/^ {3}Array" man bash should give all information needed for bash arrays. You are free to edit the answer to add any any helpful clarification. – David Ongaro Oct 13 '17 at 1:46
-1
 void Main(string[] args)
        {
            int[,] numbers = new int[,]
            {
            {0, 0, 0, 0, 1, 0, 0, 0, 1},
            {0, 0, 0, 1, 0, 0, 0, 0, 0},
            {0, 0, 0, 1, 0, 0, 0, 0, 0},
            {1, 0, 0, 0, 0, 0, 0, 0, 0},
            {1, 0, 1, 0, 0, 0, 1, 0, 0},
            {1, 0, 0, 1, 0, 0, 0, 1, 0},
            {1, 0, 0, 0, 1, 1, 1, 0, 0}
            };
            string letters = "ABCDEFGHI";
            for (int row = 0; row < 7; row++)
            {
                for (int col = 0; col < 9; col++)
                {
                    if (numbers[row, col] == 1)
                        Console.Write(letters[col]);
                }
                Console.WriteLine();
            }
        }
  • 3
    Please explain what this does and how it works. – Scott Oct 12 '17 at 3:19
  • also the language, please. – fusion.slope Oct 12 '17 at 7:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.