View Single Post
  #1   (View Single Post)  
Old 22nd February 2009
J65nko J65nko is offline
Administrator
 
Join Date: May 2008
Location: Budel - the Netherlands
Posts: 3,780
Default Simple human front-end for 'bc(1)', the unlimited precision calculator

A simple human front-end for 'bc(1)' the unlimited precision calculator.

Some weeks ago I got tired of dealing with inhumanely formatted big numbers like those presented by fdisk(1) of a 320 GB hard disk
Code:
Disk: wd1	geometry: 38913/255/63 [625142448 Sectors]
Offset: 0	Signature: 0xAA55
            Starting         Ending         LBA Info:
 #: id      C   H   S -      C   H   S [       start:        size ]
-------------------------------------------------------------------------------
 0: A5      0   1   1 -   5220 254  63 [          63:    83875302 ] FreeBSD     
*1: A6   5221   0   1 -  10441 254  63 [    83875365:    83875365 ] OpenBSD     
 2: 00      0   0   0 -      0   0   0 [           0:           0 ] unused      
 3: 00      0   0   0 -      0   0   0 [           0:           0 ] unused
Most people will find 625,142,448 or 625.142.448 easier to read then 625142448.

To solve this issue I wrote a simple shell script with pre-processes input for 'bc(1)', and post-processes it's output.

According to the man page, 'bc' is an arbitrary-precision arithmetic language and calculator. The 'bc' language supports variables, loops and functions. My goal was to only support simple single line calculations with
Code:
- + 	  : unary minus and plus
+ - * / % : addition, subtraction, multiplication, division and modulus
Input conversion

The number format expected by 'bc' does not allow the usage of comma's or periods to group the digits in groups of three. Only a period '.' is allowed to specify a decimal fraction.

The shell script 'calc' supports two number formats:
12,345,678.99, as used in the Anglo-Saxon world, and 12.345.678,99 as used in other parts of the world.

The 12,345,678.99 I will call 'commified' and 12.345.678,99 'periodified'.
Conversion of commified input for digestion by 'bc' is simple, just remove all comma's.
A 'periodified' number needs two steps: first remove all periods '.', then change the lonely ',' into a '.'

Output conversion

Conversion of the result spit out by 'bc' is not that easy.
The 'Perl cookbook' by Christiansen and presents a 'commify' perl script.
My commented version of this script
Code:
#!/usr/bin/perl -w
# $Id: commify,v 1.1 2009/02/22 02:31:30 j65nko Exp $
use strict ;

# Input style nr without ",'s :     -155566.1234
# Output style                :  -1,555,666.1234

# taken from 'Perl Cookbook' 
# (\d\d\d)(?=/d)	: group of 3 digits followed by another digit
# (?!\d*\.)		: the digits should not be followed by a decimal "."
# -155566.1234 reversed  : 4321.6665551-
#                          ^^^^ group of 3 digits followed by another digit is candidate for
# insertion of ','. Because it is followed by a decimal point, it is a decimal fraction,
# and decimal fraction shouldn't be grouped. So the '(?!\d*\.)' makes sure this does not
# happen 
# The first group of 3 digits is 666, which are followed by a '5'.
# The regex engine just looks ahead for the 5, but will not consume it.
# In the next iteration, it will start at this '5' and thus find '555' followed by the '1'.
# So a ',' will be inserted.
# The next iteration starting at the '1' finds a '-' , no other 2 digits for
# 

sub commify {
    my $nr = reverse $_[0] ;
    $nr =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g ;
    return scalar reverse $nr ;
}

while (<>) {
   print commify $_ ;
}
The script reverses the number to insert the grouping character, and then reverses the formatted number again.

This script also formed the inspiration of the 'periodify' script:
Code:
#!/usr/bin/perl -w
#$Id: periodify,v 1.1 2009/02/22 02:32:51 j65nko Exp $
use strict ;

# inspired by 'commify' routine from 'Perl Cookbook'
# first eliminate all ','            : 1,444,444.99 -> 1444444.99
# replace decimal "." by decimal ',' :   1444444.99 -> 1444444,99
# adds periods to group digits       : 1.444.444,99 

sub periodify {
    my $nr = reverse $_[0] ;
    $nr =~ s/,//g ;
    $nr =~ s/\./,/ ;
    $nr =~ s/(\d\d\d)(?=\d)(?!\d*,)/$1./g ;
    return scalar reverse $nr ;
}

while (<>) {
   print periodify $_ ;
}
The 'calc' sh script is simple:
Code:
#!/bin/sh
# $Id: calc,v 1.2 2009/02/22 02:38:06 j65nko Exp $

case "$1" in

#
-p )	# clean 1.000.000,99 -> 1000000.99  as required by 'bc' 
        CLEAN="sed -e s!\.!!g -e s!,!\.!g"
        FORMAT='periodify'	# answer produced by 'bc'
        # replace decimal point by ',' and reinsert '.' for grouping 
	;;
-c )    # clean 1,000,000.99 -> 1000000.99 as required by 'bc'
        CLEAN="sed -e s!,!!g"
        FORMAT='commify' # answer produced by 'bc'
        ;;
*)      echo "Usage: $0 -p (group digits with periods: '.')"
	echo " or"
	echo "       $0 -c (group digits with commas : ',')"
	exit 1
esac

while true ; do
    printf "Expression : " >&2
    read EXPR
    if [ -z "${EXPR}" ] ; then break
    fi
    [ -t 0 ] || echo "${EXPR}" 
    EXPR=$( echo "${EXPR}" | $CLEAN )
    #printf "Normalized : $EXPR\n"
    ANSWER=$(echo "scale=4 ; ${EXPR}" | bc )
    HUMAN=$(echo "${ANSWER}" | ${FORMAT} )
    printf "%s (  %s  )\n" ${ANSWER} ${HUMAN}
done
Some examples
  • The biggest numbers expressable in 32 and 64 bits
    Code:
    $ calc -c
    Expression : 2^32 -1
    4294967295 (  4,294,967,295  )
    Expression : 2^64 - 1
    18446744073709551615 (  18,446,744,073,709,551,615  )
  • Verifying the number of megabytes reported by 'dmesg'
    Code:
    dmesg | grep wd1
    wd1 at pciide0 channel 1 drive 0: <WDC WD3200AAKS-00VYA0>
    wd1: 16-sector PIO, LBA48, 305245MB, 625142448 sectors
    First calculate size in bytes :
    Code:
    Expression :  625142448 * 512  
    320072933376 (  320,072,933,376  )
    So 320 decimal gigabytes.
    To get the binary megabyte figure (1024^2):
    Code:
    Expression :  320,072,933,376 / 1024^2
    305245.3359 (  305,245.3359  )
  • Cylinder size of a LBA disk with 255 heads @ 63 sectors:
    Code:
    Expression : 255*63
    16065 (  16,065  )
    Size in bytes:
    Code:
    Expression : 255*63*512
    8225280 (  8,225,280  )
  • Calculating the cylinders of the 'wd1' disk, using 255 heads and 63 sectors per track:
    Code:
    Expression : 625142448 / ( 255*63)
    38913.3176 (  38,913.3176  )
    So 38,913 cylinders.
  • Dividing this 38,913 cylinder disks in about 4 equal partitions:
    Code:
    Expression : 38,913 * 255 * 63
    625137345 (  625,137,345  )
    Expression :  625,137,345 / 4
    156284336.2500 (  156,284,336.2500  )
    Another approach by dividing the number of cylinders by 4:
    Code:
    Expression :  38,913 / 4
    9728.2500 (  9,728.2500  )
    The 9,728 cylinders in sectors
    Code:
    Expression : 9,728 * 255 * 63
    156280320 (  156,280,320  )
    First partition will have 63 sectors less because of the MBR.
    Partitions two and three will both have: 156,280,320 sectors
    Size for last partition
    Code:
    Expression : 38,913 * 255 * 63 - ( 3 *  156,280,320 )
    156296385 (  156,296,385  )
    A double check
    Code:
    Expression : 38,913 * 255*63
    625137345 (  625,137,345  )
    Expression : 156,280,320 + 156,280,320 + 156,280,320 + 156,296,385
    625137345 (  625,137,345  )
  • End capital of your 1,000,000 dollar inheritance after 10 years compound interest of 4 %
    Code:
    Expression :  1,000,000 * 1.04^10
    1480000.0000 (  1,480,000.0000  )
    Same but now invested in the Bernie Madoff investment fund with a guaranteed 10%
    Code:
    Expression :  1,000,000 * 1.10^10
    2593600.0000 (  2,593,600.0000  )
Attached Files
File Type: tgz calc.tgz (1.3 KB, 64 views)
__________________
You don't need to be a genius to debug a pf.conf firewall ruleset, you just need the guts to run tcpdump
Reply With Quote