Calculating protein mass This question is part of a series solving the Rosalind challenges. For the previous question in this series, see A sequence of mistakes. The repository with all my up-to-date solutions so far can be found here.



Problem: PRTM


  In a weighted alphabet, every symbol is assigned a positive real number called a weight. A string formed from a weighted alphabet is called a weighted string, and its weight is equal to the sum of the weights of its symbols.
  
  The standard weight assigned to each member of the 20-symbol amino acid alphabet is the monoisotopic mass of the corresponding amino acid.


Given:


  A protein string \$P\$ of length at most 1000 aa.


Return:


  The total weight of \$P\$. Consult the monoisotopic mass table.


Sample Dataset:

SKADYEK


Sample Output:

821.392


My solution solves the sample dataset and the actual dataset given.

Dataset:

FQLCECNPKWNEEKRQYGACNQRNEYQDMKMVLRLNTRALAFLVEKFCTQATANSHGWQYRIENPLQLNHLPLTICRHGFDEDRIQRYQDDHNGVHIHVAMQITHSEWVLETWASSHQHANISPLSGNSQRYYPANHEGRRIQCCQFDCESHLMEDLWREATRSHESKVTRFKHDHNTPLHFTVFWPSDMEHDNGKTEYKSGWQLMFKIEGSVGKECELHTSYFSMKVKYTTVEIQTNPIERMCYAELVYWLRAEMEGVAKDRDHKWAMEHEQYIYSPMEGLINSVEYHMVYPQMARSIAKDRYWTPATLRRIHVWYCREIVQIMWIDHPKGLVCVMPDSNVGALIKWYTHYTDVKLETHQTCRKNNAGMVRDGRKKVAAEVFIIYEMGMPEYKPHEDANRDYRIPIDHPWRPHNNMCFGISGEWRRSCMKHQIQLSRWWANITKIMPMRWSIESGTTLRFVQGMVYRVNRFDDRYPRMNDCYWDAIWVCGAWTMHLTVPDLLMQMHHCMGAIHDVVFSEMGVKIAYQAVWFASVRIPASFQDWFCHEDHFQRGLRKCKLEHNDVLYETWIAMEPASYHTVHHCWHWREVFCHPCVFIHQMYSYPEFHIPDVELSLHYSQIWMNRETGWSPIRMELMCTSAPPNPDGGTPLLSFWYFGFIIQRHCGCETIMIGQWDTCFNSHHDVWAVWVHPENSQFTKERICCASFTDNSDTRECKQTVQHSSPGPTELWADSKTQVQWWYISQWLGKGLMYSEFQVSISGLSTPMLVYWLPNSMIEMNDVMEILNSPAGAQITTCIAGVNFRMIFETSWRTWYINRH


Output:

97283.202


PRTM.rb:

module Rosalind
  RESIDUE_BY_MONOISOTOPIC_MASS = {
    71.03711  =&gt; ["A"],
    103.00919 =&gt; ["C"],
    115.02694 =&gt; ["D"],
    129.04259 =&gt; ["E"],
    147.06841 =&gt; ["F"],
    57.02146  =&gt; ["G"],
    137.05891 =&gt; ["H"],
    113.08406 =&gt; ["I", "L"],
    128.09496 =&gt; ["K"],
    131.04049 =&gt; ["M"],
    114.04293 =&gt; ["N"],
    97.05276  =&gt; ["P"],
    128.05858 =&gt; ["Q"],
    156.10111 =&gt; ["R"],
    87.03203  =&gt; ["S"],
    101.04768 =&gt; ["T"],
    99.06841  =&gt; ["V"],
    186.07931 =&gt; ["W"],
    163.06333 =&gt; ["Y"]
  }

  MONOISOTOPIC_MASS_BY_RESIDUE = RESIDUE_BY_MONOISOTOPIC_MASS.
    flat_map { |c, as| as.map { |a| [a, c] } }.to_h

  def self.problem_prtm(aminoacids_string)
    aminoacids_string.
      scan(/./).
      map { |residue| MONOISOTOPIC_MASS_BY_RESIDUE[residue] }.
      take_while { |aminoacid| aminoacid != "" }.
      inject(0){ |sum, x| sum + x }
  end
end

user_input = gets.chomp
puts '%.3f' % Rosalind.problem_prtm(user_input)


The problem statement is quite similar to PROT, so the code looks quite similar to the answers given there.

Basically, I declare a bunch of value-key pairs which get inverted. This saves me from having the same value multiple times in the hash (when multiple residue weigh the same amount).

I'm mainly looking for idiomaticness and re-usability. I'm pretty sure my take_while could be written better and my naming is always open for critique.
About your code:


I am glad you liked the idea of building the inverse structure programmatically, but here I don't think it's necessary :) Only two aminoacids have the same weight, I'd write the reversed hash. 
Why residue instead of aminoacid?
I don't see why you need take_while here.
xs.reduce(0) { |acc, x| acc.method(x) } -> xs.reduce(0, :method).
Minor detail: personally I prefer explicit uses of $stdin.readline instead of gets.


I'd write:

module Rosalind
  MONOISOTOPIC_MASS_BY_AMINOACID = {
    "A" =&gt; 71.03711,
    "C" =&gt; 103.00919,
    "D" =&gt; 115.02694,
    "E" =&gt; 129.04259,
    "F" =&gt; 147.06841,
    "G" =&gt; 57.02146,
    "H" =&gt; 137.05891,
    "I" =&gt; 113.08406,
    "L" =&gt; 113.08406,
    "K" =&gt; 128.09496,
    "M" =&gt; 131.04049,
    "N" =&gt; 114.04293,
    "P" =&gt; 97.05276,
    "Q" =&gt; 128.05858,
    "R" =&gt; 156.10111,
    "S" =&gt; 87.03203,
    "T" =&gt; 101.04768,
    "V" =&gt; 99.06841,
    "W" =&gt; 186.07931,
    "Y" =&gt; 163.06333,
  }

  def self.problem_prtm(aminoacids_string)
    aminoacids_string.
      chars.
      map { |aminoacid| MONOISOTOPIC_MASS_BY_AMINOACID[aminoacid] }.
      reduce(0, :+)
  end  
end 

protein = $stdin.readline.rstrip
weight = '%.3f' % Rosalind.problem_prtm(protein)
puts(weight)


Same idea, slightly different approach:

module Rosalind
  ...
  def self.problem_prtm(aminoacids_string)
    aminoacids = aminoacids_string.chars
    masses = MONOISOTOPIC_MASS_BY_AMINOACID.values_at(*aminoacids)
    masses.reduce(0, :+)
  end
end