fishfood 0.0.0.1 → 0.0.1.0
raw patch · 14 files changed
+711/−82 lines, 14 filesdep +QuickCheckdep +mtl
Dependencies added: QuickCheck, mtl
Files
- changelog +6/−1
- copyright +2/−2
- fishfood.cabal +12/−4
- fishfood.spec +65/−0
- makefile +5/−1
- man/man1/fishfood.1 +166/−0
- src/FishFood/Data/CommandOptions.hs +47/−20
- src/FishFood/Data/Verbosity.hs +37/−0
- src/FishFood/Profiler.hs +148/−0
- src/FishFood/Test/Data/CommandOptions.hs +61/−0
- src/FishFood/Test/Data/Verbosity.hs +31/−0
- src/FishFood/Test/Profiler.hs +58/−0
- src/FishFood/Test/QuickChecks.hs +38/−0
- src/Main.hs +35/−54
changelog view
@@ -2,4 +2,9 @@ 0.0.0.1 * First version of the package.-+0.0.0.2+ * Added 'man/man1/fishfood.1' & 'fishfood.spec' to the distribution.+0.0.1.0+ * Migrated all but the I/O, from "Main" to a new module "FishFood.Profiler".+ * Added module "FishFood.Test", containing "Test.QuickCheck"-implementation.+ * Added Command-line options; '--deriveProbabilityMassFunction', '--binSizeRatio', '--runQuickChecks'.
copyright view
@@ -1,8 +1,8 @@ Author:- Dr. Alistair Ward <squeeze at functionalley dot eu>.+ Dr. Alistair Ward <fishfood at functionalley dot eu>. Copyright:- Copyright (C) 2010-2011 Dr. Alistair Ward. All Rights Reserved.+ Copyright (C) 2013 Dr. Alistair Ward. All Rights Reserved. Home-page: http://functionalley.eu
fishfood.cabal view
@@ -1,13 +1,13 @@ --Package-properties Name: fishfood-Version: 0.0.0.1+Version: 0.0.1.0 Cabal-Version: >= 1.6 Copyright: (C) 2013 Dr. Alistair Ward License: GPL License-file: LICENSE Author: Dr. Alistair Ward Stability: Unstable interface, incomplete features.-Synopsis: Finds the file-size frequency-distribution for the referenced files.+Synopsis: Calculates file-size frequency-distribution. Build-Type: Simple Description: Counts the number of files in a set of bins, each of which holds only those files which fall within a specific size-interval. Category: Utils@@ -16,7 +16,7 @@ Maintainer: fishfood <at> functionalley <dot> eu Bug-reports: fishfood <at> functionalley <dot> eu Data-files:-Extra-Source-Files: changelog copyright makefile+Extra-Source-Files: changelog copyright makefile man/man1/fishfood.1 fishfood.spec flag llvm Description: Whether the 'llvm' compiler-backend has been installed and is required for code-generation.@@ -28,13 +28,21 @@ Main-Is: Main.hs Other-modules: FishFood.Data.CommandOptions,- FishFood.Data.File+ FishFood.Data.File,+ FishFood.Data.Verbosity+ FishFood.Profiler+ FishFood.Test.Data.CommandOptions+ FishFood.Test.Data.Verbosity+ FishFood.Test.Profiler+ FishFood.Test.QuickChecks Build-depends: base == 4.*, Cabal >= 1.6 && < 2, containers, factory >= 0.2.0.5,+ mtl,+ QuickCheck, toolshed >= 0.13, unix
+ fishfood.spec view
@@ -0,0 +1,65 @@+# Copyright (C) 2013 Dr. Alistair Ward+#+# This program is free software: you can redistribute it and/or modify+# it under the terms of the GNU General Public License as published by+# the Free Software Foundation, either version 3 of the License, or+# (at your option) any later version.+#+# This program is distributed in the hope that it will be useful,+# but WITHOUT ANY WARRANTY; without even the implied warranty of+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+# GNU General Public License for more details.+#+# You should have received a copy of the GNU General Public License+# along with this program. If not, see <http://www.gnu.org/licenses/>.++%define package %name-%version+%define tarBall %package.tar.gz+%define _bindir %prefix/bin+%define _sharedir %prefix/share+%define _docdir %_sharedir/doc/%package+%define _mandir %_sharedir/man++Summary: Calculates file-size frequency-distribution.+Name: fishfood+Version: 0.0.1.0+Release: 1+License: GPLv3+#From '/usr/share/doc/packages/rpm/GROUPS'.+Group: Applications/File+Source0: http://functionalley.eu/Downloads/sdist/%tarBall+URL: http://functionalley.eu+Prefix: /usr+BuildRequires: ghc++%description+Counts the number of files in a set of bins, each of which holds only those files which fall within a specific size-interval.++%prep+#NB: we've changed directory to %_builddir+(cd $OLDPWD && runhaskell Setup sdist) && tar -zxf $OLDPWD/dist/%tarBall #Make a source-distribution & unpack it into the build-directory.+cd '%package/' && runhaskell Setup configure --user --prefix=%prefix #Configure to use the user's personal package-database & to generate an appropriate "Paths" module.++%build+cd '%package/' && runhaskell Setup build #Descend into the source-distribution and build according to the previously established configuration.++%install+cd '%package/' #Descend into the build-directory.+runhaskell Setup copy --destdir=%buildroot #Install the package in the target-directory.+mkdir -p -- '%buildroot%_docdir' && mv changelog copyright LICENSE '%buildroot%_docdir/'+mkdir -p -- '%buildroot%_mandir' && mv 'man/man1' '%buildroot%_mandir/'++%clean+make --directory='%package' clobber+rm -rf -- '%_builddir/%package/' '%buildroot'++%files+%attr(0755, root, root) %_bindir/%name+%attr(0644, root, root) %doc %_docdir/changelog+%attr(0644, root, root) %doc %_docdir/copyright+%attr(0644, root, root) %doc %_docdir/LICENSE+%attr(0644, root, root) %doc %_mandir/man1/%name.1.gz++%changelog+* Tue Jul 16 2013 Alistair Ward <fishfood at functionalley dot eu> 0.0.0.2-1+First cut.
makefile view
@@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -.PHONY: all build check clean clobber configure copy haddock help hlint install prof sdist+.PHONY: all build check clean clobber configure copy haddock help hlint install prof rpm sdist PACKAGE_NAME=fishfood @@ -49,6 +49,10 @@ check: sdist cabal upload --check --verbose=3 dist/*.tar.gz;++rpm: $(PACKAGE_NAME).spec+ rpmbuild -bb --nodeps -- $^+ mv -- `find ~/rpmbuild -type f -name '$(PACKAGE_NAME)*.rpm' -mtime 0 -print` ./ clean: runhaskell Setup $@
+ man/man1/fishfood.1 view
@@ -0,0 +1,166 @@+.TH fishfood 1+.SH NAME+\fBfishfood\fR - Calculates file-size frequency-distribution.+.SH SYNOPSIS+\fBfishfood\fR [\fIOPTIONS\fR] [\fIFile-path\fR ...]+.SH DESCRIPTION+.PP+Counts the number of the specified files which fall within each of a sequence of discrete size-intervals.+.SH OPTIONS+.TP+\fB-s\fR \fIInt\fR, \fB--binSizeIncrement=\fR\fIInt\fR+Defines the constant size-increase in the arithmetic sequence of bins into which the byte-sizes of files are categorised; defaulting to one standard-deviation.+.TP+\fB-r\fR \fIFloat\fR, \fB--binSizeRatio=\fR \fIFloat\fR+Defines the constant size-ratio in the geometric sequence of bins into which the byte-sizes of files are categorised; an alternative to "\fBbinSizeIncrement\fR".+.TP+\fB-p\fR[\fIBool\fR], \fB--deriveProbabilityMassFunction\fR[\fB=\fR\fIBool\fR]+Whether to derive the \fIProbability Mass Function\fR rather than the \fIFrequency-distribution\fR.+.br+The default value, in the absence of this option, is "\fBFalse\fR",+but in the absence of only the boolean argument, "\fBTrue\fR" will be inferred.+.TP+\fB-d\fR \fIInt\fR, \fB--nDecimalDigits=\fR\fIInt\fR+The precision to which fractional auxiliary data is displayed.+.TP+\fB--verbosity=\fR\fBSilent\fR|\fBNormal\fR|\fBVerbose\fR|\fBDeafening\fR+Produces additional output where appropriate; i.e. file-names, file-size statistics, & column-headers.+.SS "Generic Program-information"+.TP+\fB-v\fR, \fB--version\fR+Outputs version-information & then exits.+.TP+\fB-?\fR, \fB--help\fR+Displays help & then exits.+.SS "File-paths"+.TP+If \fIFile-path\fR is a single hyphen-minus (\fB-\fR), then the list of file-paths will be read from standard-input. Only plain files are acceptable; no directories, symlinks, sockets, ...+.SH EXIT-STATUS+\fB0\fR on success, & >\fB0\fR if an error occurs.+.SH EXAMPLES+.SS Example 1+To find the frequency-distribution in the size of any Matroska video-files in a file-system:+.IP+\fBfishfood --verbosity=Verbose $(find / -name '*.mkv' 2>/dev/null)\fR #CAVEAT: for efficiency, one may want to be more precise with the path supplied to "\fBfind\fR".+.nf+Files=86, mean=394459250.942, standard-deviation=304129537.729+ Bin-size Frequency+========== =========+ 0 44+ 304129538 29+ 608259076 6+ 912388614 4+1216518152 2+1520647690 1+.fi+.PP +The left-hand column defines an arithmetic sequence of bins, whilst the right-hand column defines the number of files from those specified which fall into each.+The choice of the increment between each bin has defaulted to one standard-deviation.+.br+From this data one can conclude that there are 44 files whose size lies in the semi-closed interval [0, 1) standard-deviations,+decaying monotonically to only one file whose size lies in the semi-closed interval [5, 6) standard-deviations.+.SS Example 2+One can alternatively specify the arithmetic increment between bin-sizes,+& also derive the probability that a file-size lies in any specific bin.+.IP+.B fishfood --verbosity=Verbose --binSizeIncrement=100000000 --deriveProbabilityMassFunction $(find / -name '*.mkv' 2>/dev/null)+.nf+Files=86, mean=394459250.942, standard-deviation=304129537.729+ Bin-size Probability+========== ===========+ 100000000 0.209+ 200000000 0.302+ 300000000 0.233+ 400000000 0.035+ 500000000 0.070+ 600000000 0.023+ 800000000 0.035+ 900000000 0.035+1000000000 0.012+1100000000 0.012+1200000000 0.012+1400000000 0.012+1600000000 0.012+.fi+.PP+CAVEAT: the total probability may differ from "\fB1\fR", due to round-errors; see "\fBnDecimalDigits\fR".+.SS Example 3+One can alternatively define a geometric sequence of file-size bins,+& also read the file-names from standard-input, to bypass any limit applied by the shell to the length of the command-line.+.IP+.B find /etc -type f -readable 2>/dev/null | fishfood --verbosity=Verbose --binSizeRatio=10 -+.nf+Files=1735, mean=13846.622, standard-deviation=74846.621+ Bin-size Frequency+========== =========+ 0 4 #Though "\fB0\fR" isn't a member of the requested geometric sequence, it's the integral value beneath all fractional values which are.+ 1 2+ 10 100+ 100 563+ 1000 794+ 10000 188+ 100000 83+ 1000000 1+.fi+.PP+From this data one can conclude that there are 4 files whose size lies in the semi-closed interval [0, 1) bytes, 3 files in [1, 10), 100 files in [10, 100), ...+.IP+.B find $HOME -name '*.png' -o -name '*.gif' -o -name '*.jp*g' | fishfood --verbosity=Verbose -r 2 -p -+.nf+Files=878, mean=78365.943, standard-deviation=297831.014+ Bin-size Probability+========== ===========+ 32 0.023+ 64 0.017+ 128 0.008+ 256 0.015+ 512 0.034+ 1024 0.046+ 2048 0.047+ 4096 0.096+ 8192 0.155+ 16384 0.179+ 32768 0.155+ 65536 0.157+ 131072 0.032+ 262144 0.017+ 524288 0.003+ 1048576 0.010+ 2097152 0.007+.fi+.PP+When specifying an arithmetic sequence of bin-sizes, the lack of resolution amongst smaller files makes the distribution appear like the decaying exponential of a \fIgeometric distribution\fR,+but by using a geometric sequence of bin-sizes, it can be seen more clearly to be a \fIlog-normal distribution\fR;+see "A Large-Scale Study of File-System Contents" by John R. Douceur and William J. Bolosky.+.SH AUTHOR+Written by Dr. Alistair Ward.+.SH BUGS+.SS "REPORTING BUGS"+Report bugs to \fBfishfood\fR \fIat\fR \fBfunctionalley\fR \fIdot\fR \fBeu\fR+.SH COPYRIGHT+Copyright \(co 2013 Dr. Alistair Ward+.PP+This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.+.PP+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.+.PP+You should have received a copy of the GNU General Public License along with this program. If not, see \fB<http://www.gnu.org/licenses/>\fR.+.SH "SEE ALSO"+.IP \(bu+Home-page: \fBhttp://functionalley.eu\fR+.IP \(bu+.B http://en.wikipedia.org/wiki/Interval_(mathematics)+.IP \(bu+.B http://en.wikipedia.org/wiki/Standard_deviation+.IP \(bu+.B http://en.wikipedia.org/wiki/Frequency_distribution+.IP \(bu+.B http://en.wikipedia.org/wiki/Geometric_distribution+.IP \(bu+.B http://en.wikipedia.org/wiki/Log-normal_distribution+.IP \(bu+.B http://en.wikipedia.org/wiki/Probability_mass_function+.IP \(bu+Source-documentation is generated by \fBHaddock\fR, & is available in the distribution.+.IP \(bu+.B http://www.haskell.org/haddock/
src/FishFood/Data/CommandOptions.hs view
@@ -28,14 +28,22 @@ module FishFood.Data.CommandOptions( -- * Types+-- ** Type-synonyms+ BinSizeDelta, -- ** Data-types CommandOptions( -- MkCommandOptions,- getBinSize,- getIncludeEmpty,+ getBinSizeDelta,+ getDeriveProbabilityMassFunction, getNDecimalDigits, getVerbosity- )+ ),+-- * Functions+-- ** Constructors+ mkCommandOptions,+-- ** Mutators+ setBinSizeIncrement,+ setBinSizeRatio ) where import qualified Data.Maybe@@ -44,33 +52,37 @@ import qualified ToolShed.Defaultable import qualified ToolShed.SelfValidate +-- | Either an arithmetic size-increase for which there's a default, or a geometric size-ratio.+type BinSizeDelta ratio = Either (Maybe Data.File.FileSize) ratio+ -- | Declares a record to contain command-line options.-data CommandOptions = MkCommandOptions {- getBinSize :: Maybe Data.File.FileSize, -- ^ The intervals into which file-sizes are categorised.- getIncludeEmpty :: Bool, -- ^ Whether to display empty bins.- getNDecimalDigits :: Int, -- ^ The precision to which fractional auxiliary data is displayed.- getVerbosity :: Distribution.Verbosity.Verbosity -- ^ The threshold for ancillary information-output.+data CommandOptions ratio = MkCommandOptions {+ getBinSizeDelta :: BinSizeDelta ratio, -- ^ Either the arithmetic size-increase (defaulting to one standard-deviation), or the geometric size-ratio, of the sequence of bins into which files are categorized.+ getDeriveProbabilityMassFunction :: Bool, -- ^ Whether to derive the "Probability mass function" rather than the "Frequency-distribution".+ getNDecimalDigits :: Int, -- ^ The precision to which fractional data is displayed.+ getVerbosity :: Distribution.Verbosity.Verbosity -- ^ The threshold for ancillary information-output. } deriving Show -instance ToolShed.Defaultable.Defaultable CommandOptions where+instance ToolShed.Defaultable.Defaultable (CommandOptions ratio) where defaultValue = MkCommandOptions {- getBinSize = Nothing, --Interpreted as one standard-deviation.- getIncludeEmpty = False,- getNDecimalDigits = 3,- getVerbosity = Distribution.Verbosity.normal+ getBinSizeDelta = Left Nothing, --Interpreted as one standard-deviation.+ getDeriveProbabilityMassFunction = False,+ getNDecimalDigits = 3,+ getVerbosity = Distribution.Verbosity.normal } -instance ToolShed.SelfValidate.SelfValidator CommandOptions where+instance (Num ratio, Ord ratio, Show ratio) => ToolShed.SelfValidate.SelfValidator (CommandOptions ratio) where getErrors commandOptions@MkCommandOptions {- getBinSize = binSize,- getNDecimalDigits = nDecimalDigits+ getBinSizeDelta = binSizeDelta,+ getDeriveProbabilityMassFunction = deriveProbabilityMassFunction,+ getNDecimalDigits = nDecimalDigits } = map snd $ filter fst [ (- Data.Maybe.maybe False (<= 0) binSize,- "the bin-size must exceed zero; " ++ show commandOptions ++ "."+ either (Data.Maybe.maybe False (<= 0)) (<= 1) binSizeDelta,+ "either the bin-size's arithmetic increase must exceed zero, or it's geometric ratio must exceed one; " ++ show commandOptions ++ "." ), (- nDecimalDigits < 1,- "the number of decimal digits must exceed zero; " ++ show commandOptions ++ "."+ deriveProbabilityMassFunction && nDecimalDigits < 1,+ "the number of decimal digits must exceed zero to adequately represent probabilities; " ++ show commandOptions ++ "." ), let maxNDecimalDigits = floor $ fromIntegral (@@ -84,3 +96,18 @@ ) ] +-- | Smart constructor.+mkCommandOptions :: (Num ratio, Ord ratio, Show ratio) => BinSizeDelta ratio -> Bool -> Int -> Distribution.Verbosity.Verbosity -> CommandOptions ratio+mkCommandOptions binSizeDelta deriveProbabilityMassFunction nDecimalDigits verbosity+ | ToolShed.SelfValidate.isValid commandOptions = commandOptions+ | otherwise = error $ "FishFood.Data.CommandOptions.mkCommandOptions:\t" ++ ToolShed.SelfValidate.getFirstError commandOptions+ where+ commandOptions = MkCommandOptions binSizeDelta deriveProbabilityMassFunction nDecimalDigits verbosity++-- | Mutator.+setBinSizeIncrement :: Data.File.FileSize -> CommandOptions ratio -> CommandOptions ratio+setBinSizeIncrement fileSize commandOptions = commandOptions { getBinSizeDelta = Left $ Just fileSize }++-- | Mutator.+setBinSizeRatio :: ratio -> CommandOptions ratio -> CommandOptions ratio+setBinSizeRatio ratio commandOptions = commandOptions { getBinSizeDelta = Right ratio }
+ src/FishFood/Data/Verbosity.hs view
@@ -0,0 +1,37 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-+ Copyright (C) 2013 Dr. Alistair Ward++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU General Public License as published by+ the Free Software Foundation, either version 3 of the License, or+ (at your option) any later version.++ This program is distributed in the hope that it will be useful,+ but WITHOUT ANY WARRANTY; without even the implied warranty of+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+ GNU General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+-}+{- |+ [@AUTHOR@] Dr. Alistair Ward++ [@DESCRIPTION@] Instance-definitions for 'Distribution.Verbosity.Verbosity'.+-}++module FishFood.Data.Verbosity(+-- * Constants+ range+) where++import qualified Distribution.Verbosity+import qualified ToolShed.Defaultable++instance ToolShed.Defaultable.Defaultable Distribution.Verbosity.Verbosity where+ defaultValue = Distribution.Verbosity.normal++-- | The constant complete range of values.+range :: [Distribution.Verbosity.Verbosity]+range = [minBound .. maxBound]
+ src/FishFood/Profiler.hs view
@@ -0,0 +1,148 @@+{-+ Copyright (C) 2013 Dr. Alistair Ward++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU General Public License as published by+ the Free Software Foundation, either version 3 of the License, or+ (at your option) any later version.++ This program is distributed in the hope that it will be useful,+ but WITHOUT ANY WARRANTY; without even the implied warranty of+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+ GNU General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+-}+{- |+ [@AUTHOR@] Dr. Alistair Ward++ [@DESCRIPTION@] Profiles lists of file-sizes.+-}++module FishFood.Profiler(+-- * Types+-- ** Type-synonyms+-- Probability,+-- Result,+-- FileSizeDistribution,+-- * Functions+ calculateFileSizeDistribution,+ formatFileSizeDistribution,+-- ** Accessors+ getFileSize,+ getValue+) where++import Control.Arrow((&&&),(***))+import qualified Control.Monad.Writer+import qualified Data.List+import qualified Data.Map+import qualified Data.Maybe+import qualified FishFood.Data.CommandOptions as Data.CommandOptions+import qualified FishFood.Data.File as Data.File+import FishFood.Data.Verbosity()+import qualified Text.Printf+import qualified ToolShed.Defaultable++-- | Define a type to represent the fractional closed unit-interval.+type Probability = Double++-- | Defines either the number of files or the probability that a files has a specific size.+type Result = (Data.File.FileSize, Either Int {-file-count-} Probability)++-- | Accessor.+getFileSize :: Result -> Data.File.FileSize+getFileSize = fst++-- | Accessor.+getValue :: Result -> Either Int {-file-count-} Probability+getValue = snd++-- | Defines either a /Probability Mass Function/ or /Frequency-distribution/.+type FileSizeDistribution = [Result]++-- | Calculates either the /Probability Mass Function/ or /Frequency-distribution/ for the specified files.+calculateFileSizeDistribution :: (Floating ratio, RealFrac ratio) => Data.CommandOptions.CommandOptions ratio -> [Data.File.FileSize] -> Control.Monad.Writer.Writer [String] FileSizeDistribution+calculateFileSizeDistribution commandOptions fileSizes = let+ binSizeDelta = Data.CommandOptions.getBinSizeDelta commandOptions+ deriveProbabilityMassFunction = Data.CommandOptions.getDeriveProbabilityMassFunction commandOptions+ nDecimalDigits = Data.CommandOptions.getNDecimalDigits commandOptions++ mean, standardDeviation :: Double+ (nFiles, mean, standardDeviation) = Data.File.getFileSizeStatistics fileSizes+ in do+ Control.Monad.Writer.tell [Text.Printf.printf "Files=%d, mean=%.*f, standard-deviation=%.*f" nFiles nDecimalDigits mean nDecimalDigits standardDeviation]++ return {-to Writer-monad-} $ if standardDeviation == 0+ then return {-to List-monad-} . (,) (head fileSizes) $ if deriveProbabilityMassFunction+ then Right 1 --i.e. certainty.+ else Left nFiles --i.e. all.+ else let+ getDefaultedBinSizeIncrement :: Maybe Data.File.FileSize -> Data.File.FileSize+ getDefaultedBinSizeIncrement = Data.Maybe.fromMaybe $ round standardDeviation `max` 1 {-minimum increment-} --CAVEAT: guard against subsequent division by zero or infinite iteration.++ calculatedBinSizes :: [Data.File.FileSize]+ calculatedBinSizes = map (+ \fileSize -> either (+ div {-round down-} fileSize . getDefaultedBinSizeIncrement {-non-zero-} + ) (+ floor {-round down-} . (`logBase` fromIntegral fileSize) --CAVEAT: converts file-size 0, to bin-size -infinity.+ ) binSizeDelta+ ) fileSizes --Each bin spans the semi-closed integral interval [size, succ size), so round down fractional values to match the lower bin.++ initialFrequencyDistribution :: Data.Map.Map Data.File.FileSize Int+ initialFrequencyDistribution = Data.Map.fromAscList . (+ `zip` repeat 0 --The initial file-count.+ ) . takeWhile (+ <= maximum calculatedBinSizes+ ) . dropWhile (+ < minimum calculatedBinSizes+ ) $ either (+ \maybeBinSizeIncrement -> iterate (+ getDefaultedBinSizeIncrement {-non-zero-} maybeBinSizeIncrement) 0+ ) (+ \binRatio -> map round {-file-sizes are integral-} $ iterate (* binRatio) 1 --The sequence could be started at fractional values in the open unit-interval, but the only value less than 1 which may be required is 0 (which isn't a sequence-member), which will be created later on demand.+ ) binSizeDelta++ mapBinSizeToFileSize :: Data.Map.Map Data.File.FileSize value -> Data.Map.Map Data.File.FileSize value+ mapBinSizeToFileSize = Data.Map.mapKeys $ \binSize -> either (+ (* binSize) . getDefaultedBinSizeIncrement+ ) (+ ceiling {-round up-} . (^^ binSize) --Converts binSize -infinity, back to file-size 0.+ ) binSizeDelta --Represent each bin by the minimum file-size it can accept.+ in Data.Map.toList . (+ if deriveProbabilityMassFunction+ then Data.Map.map Right . mapBinSizeToFileSize . Data.Map.map ((/ fromIntegral nFiles {-non-zero-}) . fromIntegral)+ else Data.Map.map Left . mapBinSizeToFileSize+ ) $ foldr (+ Data.Map.insertWith (+) `flip` 1 --Count the files allocated to each bin.+ ) initialFrequencyDistribution calculatedBinSizes++-- | Formats a file-size distribution.+formatFileSizeDistribution :: Data.CommandOptions.CommandOptions ratio -> FileSizeDistribution -> String+formatFileSizeDistribution commandOptions = Data.List.intercalate "\n" . map (+ \(fileSize, value) -> fileSize ++ " " ++ value+ ) . (+ if Data.CommandOptions.getVerbosity commandOptions > ToolShed.Defaultable.defaultValue+ then (+ [+ (+ ($ (fileSizeWidth, fileSizeHeader)) &&& ($ (valueWidth, valueHeader))+ ) . uncurry $ Text.Printf.printf "%*s", --Column-headers.+ (`replicate` '=') *** (`replicate` '=') $ columnWidths --Separator-bar.+ ] +++ ) --Section.+ else id+ ) . map (+ Text.Printf.printf "%*d" fileSizeWidth *** either (+ Text.Printf.printf "%*d" valueWidth+ ) (+ Text.Printf.printf "%.*f" $ Data.CommandOptions.getNDecimalDigits commandOptions+ )+ ) where+ fileSizeHeader, valueHeader :: String+ headers@(fileSizeHeader, valueHeader) = (,) "Bin-size" $ if Data.CommandOptions.getDeriveProbabilityMassFunction commandOptions then "Probability" else "Frequency"++ fileSizeWidth, valueWidth :: Int+ columnWidths@(fileSizeWidth, valueWidth) = (`max` 10) . length *** length $ headers --CAVEAT: the data-length may exceed the header-length, so define a minimum.+
+ src/FishFood/Test/Data/CommandOptions.hs view
@@ -0,0 +1,61 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-+ Copyright (C) 2013 Dr. Alistair Ward++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU General Public License as published by+ the Free Software Foundation, either version 3 of the License, or+ (at your option) any later version.++ This program is distributed in the hope that it will be useful,+ but WITHOUT ANY WARRANTY; without even the implied warranty of+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+ GNU General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+-}+{- |+ [@AUTHOR@] Dr. Alistair Ward++ [@DESCRIPTION@] Implements 'Test.QuickCheck.Arbitrary' for 'Data.CommandOptions.CommandOptions'.+-}++module FishFood.Test.Data.CommandOptions(+-- * Types+-- ** Type-synonyms+ CommandOptions+) where++import Control.Applicative((<$>), (<*>)) +import qualified Test.QuickCheck+import qualified FishFood.Data.CommandOptions as Data.CommandOptions+import FishFood.Test.Data.Verbosity()++instance (+ Enum ratio,+ Fractional ratio,+ Ord ratio,+ Show ratio,+ Test.QuickCheck.Arbitrary ratio+ ) => Test.QuickCheck.Arbitrary (Data.CommandOptions.CommandOptions ratio) where+ arbitrary = do+ deriveProbabilityMassFunction <- Test.QuickCheck.arbitrary++ Data.CommandOptions.mkCommandOptions <$> (+ either (+ Left . (succ . abs <$> {-into Maybe-monad-})+ ) (+ Right . (+ recip 1000000) . succ . abs+ ) <$> Test.QuickCheck.arbitrary {-binSizeDelta-}+ ) <*> return {-to Gen-monad-} deriveProbabilityMassFunction <*> (+ max (+ if deriveProbabilityMassFunction then 1 else 0+ ) . (+ `mod` 15 {-maximum permissible-}+ ) <$> Test.QuickCheck.arbitrary {-nDecimalDigits-}+ ) <*> Test.QuickCheck.arbitrary {-verbosity-}++-- | Defines a concrete type for testing.+type CommandOptions = Data.CommandOptions.CommandOptions Double+
+ src/FishFood/Test/Data/Verbosity.hs view
@@ -0,0 +1,31 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-+ Copyright (C) 2013 Dr. Alistair Ward++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU General Public License as published by+ the Free Software Foundation, either version 3 of the License, or+ (at your option) any later version.++ This program is distributed in the hope that it will be useful,+ but WITHOUT ANY WARRANTY; without even the implied warranty of+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+ GNU General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+-}+{- |+ [@AUTHOR@] Dr. Alistair Ward++ [@DESCRIPTION@] Implements 'Test.QuickCheck.Arbitrary' for 'Distribution.Verbosity.Verbosity'.+-}++module FishFood.Test.Data.Verbosity() where++import qualified Distribution.Verbosity+import qualified FishFood.Data.Verbosity as Data.Verbosity+import qualified Test.QuickCheck++instance Test.QuickCheck.Arbitrary Distribution.Verbosity.Verbosity where+ arbitrary = Test.QuickCheck.elements Data.Verbosity.range
+ src/FishFood/Test/Profiler.hs view
@@ -0,0 +1,58 @@+{-+ Copyright (C) 2013 Dr. Alistair Ward++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU General Public License as published by+ the Free Software Foundation, either version 3 of the License, or+ (at your option) any later version.++ This program is distributed in the hope that it will be useful,+ but WITHOUT ANY WARRANTY; without even the implied warranty of+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+ GNU General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+-}+{- |+ [@AUTHOR@] Dr. Alistair Ward++ [@DESCRIPTION@] Implements 'Test.QuickCheck.Arbitrary' & defines /QuickCheck/-properties for 'Profiler'.+-}++module FishFood.Test.Profiler(+-- * Functions+ quickChecks+) where++import qualified Control.Monad.Writer+import qualified Data.Either+import qualified Data.Set+import qualified FishFood.Data.CommandOptions as Data.CommandOptions+import qualified FishFood.Data.File as Data.File+import qualified FishFood.Profiler as Profiler+import qualified FishFood.Test.Data.CommandOptions as Test.Data.CommandOptions+import qualified Test.QuickCheck+import Test.QuickCheck((==>))+import qualified ToolShed.SelfValidate++-- | Defines invariant properties.+quickChecks :: IO ()+quickChecks = Test.QuickCheck.quickCheck `mapM_` [prop_calculateProbabilityMassFunction, prop_calculateFileSizeFrequencyDistribution, prop_attendance] where+ prop_calculateProbabilityMassFunction, prop_calculateFileSizeFrequencyDistribution, prop_attendance :: Test.Data.CommandOptions.CommandOptions -> [Data.File.FileSize] -> Test.QuickCheck.Property+ prop_calculateProbabilityMassFunction commandOptions fileSizes = not (null fileSizes) && ToolShed.SelfValidate.isValid commandOptions' ==> Test.QuickCheck.label "prop_calculateProbabilityMassFunction" . (<= recip 1000000) . (+ negate 1) . sum . Data.Either.rights {-probabilities-} . map Profiler.getValue . fst {-distribution-} . Control.Monad.Writer.runWriter . Profiler.calculateFileSizeDistribution commandOptions' $ map abs fileSizes where+ commandOptions' = commandOptions { Data.CommandOptions.getDeriveProbabilityMassFunction = True }++ prop_calculateFileSizeFrequencyDistribution commandOptions fileSizes = not (null fileSizes) && ToolShed.SelfValidate.isValid commandOptions' ==> Test.QuickCheck.label "prop_calculateFileSizeFrequencyDistribution" . (== length fileSizes) . sum . Data.Either.lefts {-frequency-} . map Profiler.getValue . fst {-distribution-} . Control.Monad.Writer.runWriter . Profiler.calculateFileSizeDistribution commandOptions' $ map abs fileSizes where+ commandOptions' = commandOptions { Data.CommandOptions.getDeriveProbabilityMassFunction = False }++ prop_attendance commandOptions fileSizes = not (null fileSizes') && ToolShed.SelfValidate.isValid commandOptions' ==> Test.QuickCheck.label "prop_attendance" . (+ == Data.Set.fromList fileSizes'+ ) . Data.Set.fromList . map Profiler.getFileSize . filter (+ (/= 0) . either fromIntegral id . Profiler.getValue --Remove file-sizes which match zero actual files.+ ) . fst {-distribution-} . Control.Monad.Writer.runWriter $ Profiler.calculateFileSizeDistribution commandOptions' fileSizes' where+ fileSizes' = map abs fileSizes++ commandOptions' :: Test.Data.CommandOptions.CommandOptions+ commandOptions' = Data.CommandOptions.setBinSizeIncrement 1 commandOptions+
+ src/FishFood/Test/QuickChecks.hs view
@@ -0,0 +1,38 @@+{-+ Copyright (C) 2013 Dr. Alistair Ward++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU General Public License as published by+ the Free Software Foundation, either version 3 of the License, or+ (at your option) any later version.++ This program is distributed in the hope that it will be useful,+ but WITHOUT ANY WARRANTY; without even the implied warranty of+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+ GNU General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+-}+{- |+ [@AUTHOR@] Dr. Alistair Ward++ [@DESCRIPTION@] Calls the /quickChecks/-functions for modules supporting this feature.+-}++module FishFood.Test.QuickChecks(+-- * Functions+ run+) where++import qualified Control.Arrow+import qualified FishFood.Test.Profiler as Test.Profiler++-- | Runs checks on invariant properties.+run :: IO ()+run = mapM_ (+ uncurry (>>) . Control.Arrow.first putStrLn+ ) [+ ("Profiler", Test.Profiler.quickChecks)+ ]+
src/Main.hs view
@@ -22,23 +22,26 @@ * Contains the entry-point of the application. * Processes the command-line arguments.++ * Delegates the task to "Profiler". -} module Main(main) where import Control.Applicative((<$>),(<*>))-import Control.Arrow((&&&)) import qualified Control.Monad+import qualified Control.Monad.Writer import qualified Data.List-import qualified Data.Map import qualified Data.Maybe import qualified Data.Version import qualified Distribution.Package import qualified Distribution.Text-import qualified Distribution.Verbosity import qualified Distribution.Version import qualified FishFood.Data.CommandOptions as Data.CommandOptions import qualified FishFood.Data.File as Data.File+import qualified FishFood.Data.Verbosity as Data.Verbosity+import qualified FishFood.Profiler as Profiler+import qualified FishFood.Test.QuickChecks as Test.QuickChecks import qualified Paths_fishfood as Paths --Either local stub, or package-instance autogenerated by 'Setup.hs build'. import qualified System.Console.GetOpt as G import qualified System.Environment@@ -49,8 +52,11 @@ import qualified ToolShed.Defaultable import qualified ToolShed.SelfValidate +-- | Define the concrete type of command-options.+type CommandOptions = Data.CommandOptions.CommandOptions Double+ -- | Used to thread user-defined command-line options, though the list of functions which implement them.-type CommandLineAction = Data.CommandOptions.CommandOptions -> IO Data.CommandOptions.CommandOptions --Supplied as the type-argument to 'G.OptDescr'.+type CommandLineAction = CommandOptions -> IO CommandOptions --Supplied as the type-argument to 'G.OptDescr'. -- | On failure to parse the specified string, returns an explanatory error. read' :: Read a => String -> String -> a@@ -83,31 +89,34 @@ progName <- System.Environment.getProgName let- defaultCommandOptions :: Data.CommandOptions.CommandOptions+ defaultCommandOptions :: CommandOptions defaultCommandOptions = ToolShed.Defaultable.defaultValue optDescrList :: [G.OptDescr CommandLineAction] optDescrList = [--- String [String] (G.ArgDescr CommandLineAction) String- G.Option "?" ["help"] (G.NoArg $ const printUsage) "Display this help, & then exit.",- G.Option "" ["verbosity"] (- setVerbosity `G.ReqArg` show [minBound :: Distribution.Verbosity.Verbosity .. maxBound]- ) ("Define the log-level; default '" ++ show (Data.CommandOptions.getVerbosity defaultCommandOptions) ++ "'."+-- String [String] (G.ArgDescr CommandLineAction) String+ G.Option "?" ["help"] (G.NoArg $ const printUsage) "Display this help, & then exit.",+ G.Option "" ["verbosity"] (+ setVerbosity `G.ReqArg` show Data.Verbosity.range+ ) ("Define the log-level; default '" ++ show (Data.CommandOptions.getVerbosity defaultCommandOptions) ++ "'." ),- G.Option "v" ["version"] (G.NoArg $ const printVersion) "Print version-information, & then exit.",- G.Option "b" ["binSize"] (setBinSize `G.ReqArg` "<Int>") "The range in bytes, accepted by any one of the bins into which file-sizes are categorised; defaulting to one standard-deviation.",- G.Option "d" ["nDecimalDigits"] (setNDecimalDigits `G.ReqArg` "<Int>") ("The precision to which fractional auxiliary data is displayed; default " ++ show (Data.CommandOptions.getNDecimalDigits defaultCommandOptions) ++ "."),- G.Option "z" ["includeEmpty"] (setIncludeEmpty `G.OptArg` "<Bool>") ("Whether to display empty bins; default '" ++ show (Data.CommandOptions.getIncludeEmpty defaultCommandOptions) ++ "'.")+ G.Option "v" ["version"] (G.NoArg $ const printVersion) "Print version-information, & then exit.",+ G.Option "s" ["binSizeIncrement"] (setBinSizeIncrement `G.ReqArg` "<Int>") "The constant size-increase in the arithmetic sequence of bins into which the byte-sizes of files are categorised; defaulting to one standard-deviation.",+ G.Option "r" ["binSizeRatio"] (setBinSizeRatio `G.ReqArg` "<Float>") "The constant size-ratio in the geometric sequence of bins into which the byte-sizes of files are categorised; an alternative to 'binSizeIncrement'.",+ G.Option "p" ["deriveProbabilityMassFunction"] (setDeriveProbabilityMassFunction `G.OptArg` "<Bool>") ("Whether to derive the \"Probability mass function\" rather than the \"Frequency-distribution\"; default '" ++ show (Data.CommandOptions.getDeriveProbabilityMassFunction defaultCommandOptions) ++ "'."),+ G.Option "d" ["nDecimalDigits"] (setNDecimalDigits `G.ReqArg` "<Int>") ("The precision to which fractional auxiliary data is displayed; default " ++ show (Data.CommandOptions.getNDecimalDigits defaultCommandOptions) ++ "."),+ G.Option "q" ["runQuickChecks"] (G.NoArg runQuickChecks) "Run Quick-checks using arbitrary data, & then exit." ] where- setBinSize, setNDecimalDigits, setVerbosity :: String -> CommandLineAction- setBinSize arg commandOptions = return {-to IO-monad-} commandOptions { Data.CommandOptions.getBinSize = Just $ readCommandArg arg }+ setBinSizeIncrement, setBinSizeRatio, setNDecimalDigits, setVerbosity :: String -> CommandLineAction+ setBinSizeIncrement arg = return {-to IO-monad-} . Data.CommandOptions.setBinSizeIncrement (readCommandArg arg)+ setBinSizeRatio arg = return {-to IO-monad-} . Data.CommandOptions.setBinSizeRatio (readCommandArg arg) setNDecimalDigits arg commandOptions = return {-to IO-monad-} commandOptions { Data.CommandOptions.getNDecimalDigits = readBoundedIntegral arg } setVerbosity arg commandOptions = return {-to IO-monad-} commandOptions { Data.CommandOptions.getVerbosity = readCommandArg arg } - setIncludeEmpty :: Maybe String -> CommandLineAction- setIncludeEmpty arg commandOptions = return {-to IO-monad-} commandOptions { Data.CommandOptions.getIncludeEmpty = Data.Maybe.maybe True readCommandArg arg }+ setDeriveProbabilityMassFunction :: Maybe String -> CommandLineAction+ setDeriveProbabilityMassFunction arg commandOptions = return {-to IO-monad-} commandOptions { Data.CommandOptions.getDeriveProbabilityMassFunction = Data.Maybe.maybe True readCommandArg arg } - printVersion, printUsage :: IO Data.CommandOptions.CommandOptions+ printVersion, printUsage :: IO CommandOptions printVersion = Text.Printf.printf "%s\n\n%s %s.\n%s.\n%s.\n%s %s.\n" packageName "Copyright (C) 2013" author "This program comes with ABSOLUTELY NO WARRANTY" "This is free software, & you are welcome to redistribute it under certain conditions" "Written by" author >> System.Exit.exitWith System.Exit.ExitSuccess where packageIdentifier :: Distribution.Package.PackageIdentifier packageIdentifier = Distribution.Package.PackageIdentifier {@@ -121,6 +130,9 @@ printUsage = System.IO.hPutStrLn System.IO.stderr ("Usage:\t" ++ G.usageInfo progName optDescrList ++ " [<File-path> ...]") >> System.Exit.exitWith System.Exit.ExitSuccess + runQuickChecks :: CommandOptions -> IO CommandOptions+ runQuickChecks _ = Test.QuickChecks.run >> System.Exit.exitWith System.Exit.ExitSuccess+ args <- System.Environment.getArgs case G.getOpt G.RequireOrder optDescrList args of@@ -151,43 +163,12 @@ else return filePaths else {-real fileNames-} return {-to IO-monad-} nonOptions - let (nDecimalDigits, verbosity) = Data.CommandOptions.getNDecimalDigits &&& Data.CommandOptions.getVerbosity $ commandOptions --Deconstruct.-- Control.Monad.when (verbosity == maxBound) $ System.IO.hPrint System.IO.stderr filePaths --CAVEAT: potentially very long.-- fileSizes <- mapM Data.File.findSize $ Data.List.nub filePaths --De-duplicate the file-paths, & find the corresponding file-sizes.-- let- mean, standardDeviation :: Double- (nFiles, mean, standardDeviation) = Data.File.getFileSizeStatistics fileSizes-- columnHeaders :: [String]- columnHeaders = ["Bin", "Frequency"]-- columnWidths :: [Int]- columnWidths = map length columnHeaders-- formatLine :: Data.File.FileSize -> Int -> IO ()- formatLine bin = Text.Printf.printf "%*d\t%*d\n" (head columnWidths) bin (last columnWidths)-- Control.Monad.when (verbosity >= Distribution.Verbosity.verbose) $ do- Control.Monad.void $ Text.Printf.hPrintf System.IO.stderr "Files=%d, mean=%.*f, standard-deviation=%.*f\n" nFiles nDecimalDigits mean nDecimalDigits standardDeviation --Print statistics.-- let tabulate = Data.List.intercalate "\t"-- Text.Printf.hPrintf System.IO.stderr "%s\n%s\n" (tabulate columnHeaders) (tabulate $ map (`replicate` '=') columnWidths) --Print column-headers.+ Control.Monad.when (Data.CommandOptions.getVerbosity commandOptions == maxBound) $ System.IO.hPrint System.IO.stderr filePaths --CAVEAT: potentially very long. - if standardDeviation == 0- then formatLine 0 nFiles- else let- bins = map (`div` Data.Maybe.fromMaybe (round standardDeviation) (Data.CommandOptions.getBinSize commandOptions)) fileSizes+ (fileSizeDistribution, statistics) <- Control.Monad.Writer.runWriter . Profiler.calculateFileSizeDistribution commandOptions <$> mapM Data.File.findSize (Data.List.nub filePaths) - frequencyDistribution- | Data.CommandOptions.getIncludeEmpty commandOptions = Data.Map.fromList $ [0 .. maximum bins] `zip` repeat 0- | otherwise = Data.Map.empty- in mapM_ (uncurry formatLine) . Data.Map.toList $ foldr (- uncurry (Data.Map.insertWith (+))- ) frequencyDistribution $ bins `zip` repeat 1+ Control.Monad.when (Data.CommandOptions.getVerbosity commandOptions > ToolShed.Defaultable.defaultValue) $ mapM_ (System.IO.hPutStrLn System.IO.stderr) statistics + System.IO.putStrLn $ Profiler.formatFileSizeDistribution commandOptions fileSizeDistribution (_, _, errors) -> System.IO.Error.ioError . System.IO.Error.userError $ concatMap init {-chop-} errors