Displaying File Sizes

how-to
foundation
asobjc

(Mark Alldritt) #1

Displaying file sizes can be challenging, because the units you use can depend on the actual value. And sometimes you want the values based on 1024 bytes-to-the-kilobyte, while others you might want the decimal value generally used to measure storage devices. NSByteCountFormatter can take care of all this for you.

To get the binary value, use:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set theString to (current application's NSByteCountFormatter's stringFromByteCount:123456789 countStyle:(current application's NSByteCountFormatterCountStyleDecimal)) as text
--> "123.5 MB"

And for the decimal value:

set theString to (current application's NSByteCountFormatter's stringFromByteCount:123456789 countStyle:(current application's NSByteCountFormatterCountStyleBinary)) as text
--> "117.7 MB"

You can also get more advanced formats, for example:

my formatAsBytes:123456789
--> "123.5 MB (123,456,789 bytes)"
on formatAsBytes:theValue
	set theNSByteCountFormatter to current application's NSByteCountFormatter's new()
	theNSByteCountFormatter's setIncludesActualByteCount:true
	return (theNSByteCountFormatter's stringFromByteCount:theValue) as text
end formatAsBytes:

(Jim Underwood) #2

Thanks, Mark.
Is there a way to specify the output units, like “KB”, or “MB”, or “GB”?


(Mark Alldritt) #3

Sure, just set the NSByteCountFormatter’s allowedUnits property:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"

-- classes, constants, and enums used
property NSByteCountFormatterUseKB : a reference to 2

my formatAsBytes:123456789
--> "123,457 KB (123,456,789 bytes)"
on formatAsBytes:theValue
	set theNSByteCountFormatter to current application's NSByteCountFormatter's new()
	theNSByteCountFormatter's setIncludesActualByteCount:true
	theNSByteCountFormatter's setAllowedUnits:NSByteCountFormatterUseKB
	return (theNSByteCountFormatter's stringFromByteCount:theValue) as text
end formatAsBytes:

(Jim Underwood) #4

Sorry to trouble you more, but can you share the value for MB and GB?

I really hate the terrible ObjC documentation provided by Apple. I googled and found, this reference, but it doesn’t tell us the values, or how to find them:
allowedUnits - NSByteCountFormatter | Apple Developer Documentation


(Mark Alldritt) #5

They are a bit mask. I think MB is 4 and GB is 8. But there is a way to find for yourself: delete the KB in NSByteCountFormatterUseKB and then press Escape to see what completions are offered.


(Jim Underwood) #6

Thanks, Mark. Great tip! :+1:

To All:

Here’s my refactor of Mark’s script, adding the option for different units.

EDIT: Moved nsUnitsRef to handler.

### Refactor of Script by @alldritt on  2017-12-21 ###

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set fileSizeNum to missing value

--set fileSizeNum to 123456789

if (fileSizeNum is missing value) then
  set filePath to POSIX path of (choose file)
  set oFile to info for filePath
  set fileSizeNum to size of oFile
end if

set fileSizeMBStr to my convertBytesToString(fileSizeNum, "MB", true)

on convertBytesToString(pSizeNum, pNSUnitsStr, pIncludeBytesBool)
  
  if (pNSUnitsStr = "KB") then
    set nsUnits to a reference to 2
  else if (pNSUnitsStr = "MB") then
    set nsUnits to a reference to 4
  else if (pNSUnitsStr = "GB") then
    set nsUnits to a reference to 8
  else
    set nsUnits to a reference to 4 -- default to MB
  end if
  
  set theNSByteCountFormatter to current application's NSByteCountFormatter's new()
  theNSByteCountFormatter's setIncludesActualByteCount:pIncludeBytesBool
  theNSByteCountFormatter's setAllowedUnits:nsUnits
  return (theNSByteCountFormatter's stringFromByteCount:pSizeNum) as text
  
end convertBytesToString

(Shane Stanley) #7

Just in case it’s not obvious, the original version Mark posted will use whichever unit makes sense given the number of bytes. If the number was 1234567890 it would use GB, and for 123456 it would use KB.

So there’s no reason to specify the the unit unless you must have particular units.


(Jim Underwood) #8

Just one more question: How do we set the format of the output string? :wink:

Currently it is set to one decimal, but I’d like 3 decimals, so when the size is < 1 I still get a meaningful value.

So, instead of showing “1.5 MB” it would show “1.523 MB”, or more importantly for 123,456 bytes it would show “0.118 MB”.

Again, I tried to find the answer by searching the obscure ObjC documentation, but could not.

Thanks.


(Jim Underwood) #9

Thanks, Shane, I saw that. But often I will want to show the size of multiple files, and I want the units and format to be constant.


(Shane Stanley) #10

Try theNSByteCountFormatter's setAdaptive:false. It defaults to true, and the docs for it say:

The "adaptive" algorithm is platform specific and uses a different number of fraction digits based on the magnitude (in OS X v10.8: 0 fraction digits for bytes and KB; 1 fraction digits for MB; 2 for GB and above). Otherwise the result always tries to show at least three significant digits, introducing fraction digits as necessary.

You may also need to use theNSByteCountFormatter's setZeroPadsFractionDigits.


(Jim Underwood) #11

EDIT:

Thanks, Shane. That did the trick (almost). :wink:

It looks like this ensure there are the same number of significant digits, so I get this:
0.000123 GB
OR
0.123 MB

set fileSizeNum to 123456
set fileSizeMBStr to my convertBytesToString(fileSizeNum, "GB", false)
-->0.000123 GB

If I use a size of 1234567
I get:
1.23 MB

What I’d like to see for 123456:
0.000 GB
0.123 MB

It doesn’t appear that we need that. I tried both:
theNSByteCountFormatter's setZeroPadsFractionDigits:false
AND
theNSByteCountFormatter's setZeroPadsFractionDigits:true

It did not make any difference.


(Jim Underwood) #12

Just made an edit in my above post.


(Shane Stanley) #13

If you have a spare $25, you might want to look at Dash. You need to have Xcode installed to download the Objective-C documentation, but then you can search directly from Script Debugger using option-click. It covers a zillion languages, including Javascript and AppleScript – it effectively gives you a searchable version of the ASLG for the latter.


(Jim Underwood) #14

I already have Dash, but it doesn’t solve the problem. It just makes getting the ObjC page faster, but still shows same, incomplete info.


(Jim Underwood) #15

Actually, it is simple math problem. :wink:

Thanks for providing a simple solution that provides a quick answer, that will be fine for many use cases.

In my use case, I need control over both the units and the format, and that turns out to be a challenge using the ObjC NSByteCountFormatter method.

So, I just wrote a simple solution using math. But it turns out that because native AppleScript is so limited, we have to write handlers and resort to ASObjC to handle simple stuff. So, my method turns out longer than yours. It does more, but requires more lines of code.

FWIW, here is my solution:

Revised 2017-12-23 20:14 GMT-0600, Ver 1.2

Use method suggested by @NigelGarvey

property ptyScriptName : "Convert Bytes to Specified Units"
property ptyScriptVer : "1.2" -- use byte conversion method by @NigelGarvey
property ptyScriptDate : "2017-12-23"
property ptyScriptAuthor : "JMichaelTX"

use AppleScript version "2.5" -- El Capitan (10.11) or later
use framework "Foundation" -- this may not be required
use framework "AppKit" -- this may not be required
use scripting additions

## Some Scripts may work with Yosemite, but no guarantees ##
#  This script has been tested ONLY in macOS 10.11.6+

---set fileSizeBytes to 995829

--- GET FILE SELECTED in FINDER ---

tell application "Finder" to set finderSelectionList to selection as alias list
if length of finderSelectionList = 0 then error "No files were selected in the Finder!"
set fileAlias to item 1 of finderSelectionList

set fileSizeBytes to size of (get info for fileAlias)

--- CONVERT BYTES TO OTHER UNITS ---
set fileSize to my convertBytes(fileSizeBytes, "MB", "str")
-->995829 returns "0.950 MB"

--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
on convertBytes(pSizeBytes, pUnitsStr, pOutputType)
  (*  VER: 1.1    2017-12-23
  -------------------------------------------------------------------------------
  PURPOSE:  Convert Bytes into other units
  PARAMETERS:
    • pSizeBytes  |  num      | size in bytes
    • pUnitsStr  | text    | Units to convert to.  Must be one of these:
        • "KB", "MB", "GB", "TB"
    • pOutputType  |  text  | Type of output. Must start with one of these:
        • "str", "num"
        
  RETURNS:  | text OR num | Size converted to specified units, rounded to 3 places.
      • IF pOutputType starts with "str", then returned as text with units label
  
  AUTHOR:  JMichaelTX
—————————————————————————————————————————————————————————————————————————————————
*)
  
  --- Better Method THanks to @NigelGarvey ---
  
  set converUnits to "KB MB GB TB"
  set convFactor to 1024 ^ ((offset of pUnitsStr in converUnits) div 3 + 1)
  
  set fileSize to pSizeBytes / convFactor
  
  if (pOutputType starts with "Str") then
    set fileSize to my formatNumber(fileSize, "#,##0.000;0.000;(#,##0.000)") & " " & pUnitsStr
  else
    set fileSize to roundThis(fileSize, 3)
  end if
  
  return fileSize
end convertBytes
--~~~~~~~~~~~~~~~ END OF handler convertBytes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


on formatNumber(pNumber, pFormatStr)
  # REF:  Everyday AppleScriptObjC 3rd Edition by Shane Stanley
  
  set theFormatter to current application's NSNumberFormatter's new()
  theFormatter's setFormat:pFormatStr
  theFormatter's setLocalizesFormat:false
  set theResult to theFormatter's stringFromNumber:pNumber
  return theResult as text
end formatNumber

on roundThis(n, numDecimals)
  # REF:  http://macscripter.net/viewtopic.php?id=24415
  
  set x to 10 ^ numDecimals
  (((n * x) + 0.5) div 1) / x
end roundThis

(Nigel Garvey) #16

Hi Jim.

May I suggest : ?

set converUnits to "KB MB GB TB"

set convFactor to 1024 ^ ((offset of pUnitsStr in converUnits) div 3 + 1)

This would make it simple to add another parameter specifying the “K” size: 1024 (as in your script) or 1000 (as used for a while now by macOS):

set fileSize to my convertBytes(fileSizeBytes, 1024, "MB", "str")

--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
on convertBytes(pSizeBytes, pKSize, pUnitsStr, pOutputType)
	set converUnits to "KB MB GB TB"
	
	set convFactor to pKSize ^ ((offset of pUnitsStr in converUnits) div 3 + 1)
	-- etc.

(Shane Stanley) #17

Bear in mind that system supplied formatters are designed to perform two other important roles: localization, and consistency of display between applications. So if someone in Japan runs the latter handler in Mark’s post, instead of 123.5 MB (123,456,789 bytes), they will see 123.5 MB(123,456,789バイト).

When you’re writing scripts for your own use, these things may not matter. But when you’re distributing stuff, it can make a difference.


(Jim Underwood) #18

Nigel, yes, you may suggest. I always welcome your suggestions and improvements to my scripts and/or questions.
Thank you so much for taking the time to even read/review my script, and then offering a much better, much more elegant, method.

While I expected your method to be much faster, to my surprise Script Geek reports both methods take the same time: 0.004 sec. (10 run avg).

So I’m definitely going to use your method. :+1:

EDIT:
I have updated my post above to use Nigel’s method:
Revised 2017-12-23 20:14 GMT-0600, Ver 1.2


(Jim Underwood) #19

Excellent points. I definitely have use cases for Mark’s original script.