4
\$\begingroup\$

This is a tiny etude on a theme given here, using Python: Provide a function that generates a human readable file size such as 10.0GiB, 13B, 42.2TiB from a given integer number. (A similar Ruby etude is here.) The function should

  • gracefully handle any input (even negative numbers, although they don't make sense for counting bytes - maybe they could represent negative offsets)
  • print numbers smaller than 1024 without a decimal point
  • print all other numbers with one decimal place and the appropriate unit (KiB, MiB, etc.)

It's more verbose than the python counterpart, so the question is whether this can be made more terse.

    humanReadableBytes :: Integer -> String
    humanReadableBytes size | null pairs = printf "%.0fZiB" (size'/1024^7)
                            | otherwise  = if unit=="" then printf "%dB" size 
                                           else printf "%.1f%sB" n unit
    where
        (n, unit) = head pairs
        pairs = dropWhile ((1024<).abs.fst) 
             (zip (map ((size'/).(1024^)) [0..]) units) :: [(Double, String)]
        size' = fromIntegral size
        units = ["","Ki","Mi","Gi","Ti","Pi","Ei","Zi"]

Right away it can be noted that the use of String is suboptimal, but Text.Printf does not allow Text, so printf ought to be replaced.

\$\endgroup\$
5
\$\begingroup\$

We begin with the "loop". map ((size'/).(1024^)) [0..] is iterate (/1024) size. This has not necessarily the same effect, since (x/y)/y is not the same as x/y² when using floating point arithmetic, but it's fine for our cause. It also works like the Python code, which was your original intention.

We now have

humanReadableBytes :: Integer -> String
humanReadableBytes size | null pairs = printf "%.0fZiB" (size'/1024^7)
                        | otherwise  = if unit=="" then printf "%dB" size 
                                       else printf "%.1f%sB" n unit
where
    (n, unit) = head pairs
    pairs = zip (iterate (/1024) 'size)) units
    size' = fromIntegral size :: Double
    units = ["","Ki","Mi","Gi","Ti","Pi","Ei","Zi"]

And that's all we can improve in terms of terseness at that point without changing your code fundamentally. The main difference between the Python and the Haskell implementation is the mutation of the original num and the early exit from the function.

I would recommend to check the "B" case earlier by the way:

humanReadableBytes :: Integer -> String
humanReadableBytes size
  | abs size < 1024 = printf "%dB" size 
  | null pairs      = printf "%.0fZiB" (size'/1024^7)
  | otherwise       = printf "%.1f%sB" n unit
where
  ...
\$\endgroup\$

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.