Archive for the 'ソート(sorting)' Category

2017/10/10 なろう小説APIで各カテゴリごとの集計を実行

「小説家になろう」サイトのAPI「なろう小説API」を呼び出して、カテゴリごとの該当件数を集計するAppleScriptです。

「なろう小説API」は事前にAPI Keyの取得も不要で、簡単に呼び出せるのでお手軽に使えます。

本AppleScriptは、「小説家になろう」掲載作品を、大カテゴリと小カテゴリでコードを指定して、ループで存在件数の集計を行います。カテゴリごとに分布が偏っているようなので、該当件数が0件のカテゴリは結果出力しないようにしています。筆者の環境では集計に22〜25秒ぐらいかかっています(インターネット接続回線速度に依存)。

http headerにgzip転送リクエスト要求を書きつつ、実際のデータ自体もgzipで圧縮されているので、二重に圧縮している状態です。実測したところ、http headerでgzip指定を行なったほうがトータルで1秒程度速かったので「そんなもんかな」と思いつつ、そのままにしています。

Web APIからのデータ受信時のNSDataからのZip展開にオープンソースのフレームワーク「GZIP」(By Nick Lockwood)を利用しています。同プロジェクトはGithub上のXcodeプロジェクトをXcodeでビルドするとFrameworkが得られるので、ビルドして~/Library/FrameworksフォルダにGZIP.frameworkを入れてください。

ジャンルは数値で指定するようになっていますが、その数値が何を示しているかという情報はAPI側からの出力にはないので、Webサイト上から文字情報をコピペで取得し、AppleScript内に記載して(ハードコーディング)カテゴリコードリストと照合して出力しています。

実際に集計してみると、ノンカテゴリが53%ということで、カテゴリ分けの機能が有効に活用されていないことが見てとれます。

そのことについては運営側も重々承知しているようで、APIの検索オプションに「キーワードに異世界転生があるものを含む」といったものがあるなど、ジャンルよりもキーワード重視するようにしているようです。

そういいつつも、使われているキーワードについては若干の表記ゆらぎがあるようで、単純にこのオプションを指定しても「異世界転生もの」をすべて抽出できていないように見えます。キーワード自体にどの程度「表記揺れ」が存在しているのかを調べてみるとよいのかもしれません。

APIの仕様上、2,000件しか詳細データを取得できないように見えるので、そのあたりがちょっと気になります(どうも全数調査を行いにくい仕様)。

分析するまでもなく、異世界転生ものが多く、ノンジャンル作品でも異世界転生ものばっかりという印象です。掘り出しもので「ソ連の宇宙技術は最強過ぎたのだが、それを西側諸国が完全に理解したのはつい最近だった」という作品に行き当たり、これが強烈に面白いです。

AppleScript名:なろう小説APIで各カテゴリごとの集計を実行
– Created 2017-10-10 by Takaaki Naganoya
– 2017 Piyomaru Software
–http://piyocast.com/as/archives/4891
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “GZIP”
–https://github.com/nicklockwood/GZIP
–http://dev.syosetu.com/man/api/
–1日の利用上限は80,000または転送量上限400MByte???

property |NSURL| : a reference to current application’s |NSURL|
property NSString : a reference to current application’s NSString
property NSArray : a reference to current application’s NSArray
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSURLQueryItem : a reference to current application’s NSURLQueryItem
property NSURLComponents : a reference to current application’s NSURLComponents
property NSJSONSerialization : a reference to current application’s NSJSONSerialization
property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest
property NSURLConnection : a reference to current application’s NSURLConnection
property NSSortDescriptor : a reference to current application’s NSSortDescriptor
property NSNumber : a reference to current application’s NSNumber
property NSNumberFormatter : a reference to current application’s NSNumberFormatter
property NSNumberFormatterRoundUp : a reference to current application’s NSNumberFormatterRoundUp
property NSNumberFormatterRoundDown : a reference to current application’s NSNumberFormatterRoundDown

set invList to {}

set bgList to {1, 2, 3, 4, 99, 98}
set bigGnereLabel to {“恋愛”, “ファンタジー”, “文芸”, “SF”, “その他”, “ノンジャンル”}

set gList to {101, 102, 201, 202, 301, 302, 303, 304, 305, 306, 307, 401, 402, 403, 404, 9901, 9902, 9903, 9904, 9999, 9801}
set smlGenreLabel to {“異世界〔恋愛〕”, “現実世界〔恋愛〕”, “ハイファンタジー〔ファンタジー〕”, “ローファンタジー〔ファンタジー〕”, “純文学〔文芸〕”, “ヒューマンドラマ〔文芸〕”, “歴史〔文芸〕”, “推理〔文芸〕”, “ホラー〔文芸〕”, “アクション〔文芸〕”, “コメディー〔文芸〕”, “VRゲーム〔SF〕”, “宇宙〔SF〕”, “空想科学〔SF〕”, “パニック〔SF〕”, “童話〔その他〕”, “詩〔その他〕”, “エッセイ〔その他〕”, “リプレイ〔その他〕”, “その他〔その他〕”, “ノンジャンル〔ノンジャンル〕”}

–全体の件数取得
set aRec to {gzip:“5″, out:“json”, lim:“1″}
set aRESTres to callNarouAPI(aRec, “1″, “1″) of me
set wholeCount to (allCount of first item of aRESTres)

–カテゴリごとの集計
repeat with i in bgList
  repeat with ii in gList
    set aRec to {gzip:“5″, biggenre:i as string, genre:ii as string, out:“json”, lim:“1″}
    
set aRESTres to callNarouAPI(aRec, “1″, “1″) of me
    
set aTotal to allCount of first item of aRESTres
    
    
if aTotal is not equal to 0 then
      set big to contents of i
      
set small to contents of ii
      
set bigLabel to getLabelFromNum(bgList, bigGnereLabel, big) of me
      
set smlLabel to getLabelFromNum(gList, smlGenreLabel, small) of me
      
set aPerCentatge to roundingDownNumStr(((aTotal / wholeCount) * 100), 1) of me
      
set the end of invList to {biggenre:bigLabel, genre:smlLabel, totalNum:aTotal, percentage:aPerCentatge}
    end if
  end repeat
end repeat

set bList to sortRecListByLabel(invList, “totalNum”, false) of me –降順ソート
–> {{totalNum:274072, biggenre:”ノンジャンル”, percentage:53.1, genre:”ノンジャンル〔ノンジャンル〕”}, {totalNum:47121, biggenre:”ファンタジー”, percentage:9.1, genre:”ハイファンタジー〔ファンタジー〕”}, {totalNum:28883, biggenre:”恋愛”, percentage:5.6, genre:”現実世界〔恋愛〕”}, {totalNum:23217, biggenre:”文芸”, percentage:4.5, genre:”ヒューマンドラマ〔文芸〕”}, {totalNum:21320, biggenre:”ファンタジー”, percentage:4.1, genre:”ローファンタジー〔ファンタジー〕”}, {totalNum:17079, biggenre:”恋愛”, percentage:3.3, genre:”異世界〔恋愛〕”}, {totalNum:16798, biggenre:”その他”, percentage:3.2, genre:”その他〔その他〕”}, {totalNum:13892, biggenre:”その他”, percentage:2.6, genre:”詩〔その他〕”}, {totalNum:13341, biggenre:”文芸”, percentage:2.5, genre:”コメディー〔文芸〕”}, {totalNum:10120, biggenre:”文芸”, percentage:1.9, genre:”ホラー〔文芸〕”}, {totalNum:9502, biggenre:”その他”, percentage:1.8, genre:”エッセイ〔その他〕”}, {totalNum:8486, biggenre:”文芸”, percentage:1.6, genre:”純文学〔文芸〕”}, {totalNum:7211, biggenre:”文芸”, percentage:1.3, genre:”アクション〔文芸〕”}, {totalNum:6199, biggenre:”SF”, percentage:1.2, genre:”空想科学〔SF〕”}, {totalNum:5780, biggenre:”その他”, percentage:1.1, genre:”童話〔その他〕”}, {totalNum:3295, biggenre:”文芸”, percentage:0.6, genre:”推理〔文芸〕”}, {totalNum:3217, biggenre:”文芸”, percentage:0.6, genre:”歴史〔文芸〕”}, {totalNum:2606, biggenre:”SF”, percentage:0.5, genre:”VRゲーム〔SF〕”}, {totalNum:1471, biggenre:”SF”, percentage:0.2, genre:”パニック〔SF〕”}, {totalNum:1454, biggenre:”SF”, percentage:0.2, genre:”宇宙〔SF〕”}, {totalNum:190, biggenre:”その他”, percentage:0.0, genre:”リプレイ〔その他〕”}}

on callNarouAPI(aRec, callFrom, callNum)
  set reqURLStr to “http://api.syosetu.com/novelapi/api/” –通常API
  
  
–set aRec to {gzip:”5″, |st|:callFrom as string, out:”json”, lim:callNum as string}
  
set aURL to retURLwithParams(reqURLStr, aRec) of me
  
set aRes to callRestGETAPIAndParseResults(aURL) of me
  
  
set aRESCode to (responseCode of aRes) as integer
  
if aRESCode is not equal to 200 then return false
  
  
set aRESHeader to responseHeader of aRes
  
set aRESTres to (json of aRes) as list
  
end callNarouAPI

–GET methodのREST APIを呼ぶ
on callRestGETAPIAndParseResults(aURL)
  set aRequest to NSMutableURLRequest’s requestWithURL:(|NSURL|’s URLWithString:aURL)
  
aRequest’s setHTTPMethod:“GET”
  
aRequest’s setValue:“gzip” forHTTPHeaderField:“Content-Encoding”
  
  
set aRes to NSURLConnection’s sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value)
  
set resList to aRes as list
  
  
set bRes to contents of (first item of resList)
  
  
set rRes to bRes’s gunzippedData() –From GZIP.framework
  
  
set resStr to NSString’s alloc()’s initWithData:rRes encoding:(NSUTF8StringEncoding)
  
  
set jsonString to NSString’s stringWithString:resStr
  
set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding)
  
set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value)
  
  
–Get Response Code & Header
  
set dRes to contents of second item of resList
  
if dRes is not equal to missing value then
    set resCode to (dRes’s statusCode()) as number
    
set resHeaders to (dRes’s allHeaderFields()) as record
  else
    set resCode to 0
    
set resHeaders to {}
  end if
  
  
return {json:aJsonDict, responseCode:resCode, responseHeader:resHeaders}
end callRestGETAPIAndParseResults

on retURLwithParams(aBaseURL, aRec)
  set aDic to NSMutableDictionary’s dictionaryWithDictionary:aRec
  
  
set aKeyList to (aDic’s allKeys()) as list
  
set aValList to (aDic’s allValues()) as list
  
set aLen to length of aKeyList
  
  
set qList to {}
  
repeat with i from 1 to aLen
    set aName to contents of item i of aKeyList
    
set aVal to contents of item i of aValList
    
set the end of qList to (NSURLQueryItem’s queryItemWithName:aName value:aVal)
  end repeat
  
  
set aComp to NSURLComponents’s alloc()’s initWithString:aBaseURL
  
aComp’s setQueryItems:qList
  
set aURL to (aComp’s |URL|()’s absoluteString()) as text
  
  
return aURL
end retURLwithParams

–リストに入れたレコードを、指定の属性ラベルの値でソート
on sortRecListByLabel(aRecList as list, aLabelStr as string, ascendF as boolean)
  set aArray to NSArray’s arrayWithArray:aRecList
  
  
set sortDesc to NSSortDescriptor’s alloc()’s initWithKey:aLabelStr ascending:ascendF
  
set sortDescArray to NSArray’s arrayWithObjects:sortDesc
  
set sortedArray to aArray’s sortedArrayUsingDescriptors:sortDescArray
  
  
set bList to sortedArray as list
  
return bList
end sortRecListByLabel

on getLabelFromNum(aList, labelLIst, aNum)
  set aInd to offsetOf(aList, aNum) of me
  
set anItem to contents of item aInd of labelLIst
  
return anItem
end getLabelFromNum

on offsetOf(aList as list, aTarg)
  set aArray to current application’s NSArray’s arrayWithArray:aList
  
set aIndex to aArray’s indexOfObjectIdenticalTo:aTarg
  
return (aIndex + 1)
end offsetOf

on roundingDownNumStr(aNum as string, aDigit as integer)
  set a to NSString’s stringWithString:aNum
  
set aa to a’s doubleValue()
  
set aFormatter to NSNumberFormatter’s alloc()’s init()
  
aFormatter’s setMaximumFractionDigits:aDigit
  
aFormatter’s setRoundingMode:(NSNumberFormatterRoundDown)
  
set aStr to aFormatter’s stringFromNumber:aa
  
return (aStr as text) as real
end roundingDownNumStr

on roundingUpNumStr(aNum as string, aDigit as integer)
  set a to NSString’s stringWithString:aNum
  
set aa to a’s doubleValue()
  
set aFormatter to NSNumberFormatter’s alloc()’s init()
  
aFormatter’s setMaximumFractionDigits:aDigit
  
aFormatter’s setRoundingMode:(NSNumberFormatterRoundUp)
  
set aStr to aFormatter’s stringFromNumber:aa
  
return (aStr as text) as real
end roundingUpNumStr

★Click Here to Open This Script 

2017/09/06 ツイ4のページで新規連載マンガの画像を取得してPDF化(新規連載のPDF化)v3

Safariで表示中のWebマンガサイト「ツイ4」(更新情報をTwitterに投稿)のマンガを全エピソードダウンロードしてPDFにまとめるAppleScriptです。

実行にあたってはShane StanleyのAppleScript Libraries「BridgePlus」のインストールを必要とします(~/ibrary/Script Librariesフォルダに入れるだけ)。

実行開始時にはSafariでツイ4の特定のマンガのページをオープンしている必要があります。

tui4.png

Safariの最前面のウィンドウからURLやTitle、リンクされている画像の詳細情報を取得し、条件チェックなどを行なったのちに詳細なデータの抽出を行います。

次に、PDFの保存先を選択するダイアログを表示。このさい、デフォルトの保存先を「ピクチャ」フォルダ、ファイル名をマンガのタイトルに指定。

ページにリンクされていた画像(ツイではファイル名はシーケンシャル番号)から番号の情報だけを抽出して最大値、最小値を計算。この範囲で画像のダウンロード、PDFへの追記を行います。ただし、実運用してみたところ、Safariからすべての画像を取得できないようで(非同期表示しているようなので)、とりあえず1〜9999までの番号の画像を順次ダウンロードし、画像が存在しなければ処理を終了しています。

画像をダウンロードするたびにPDFに追記していますが、このあたりは途中でエラーが出て停止してもそれまでの処理内容が保存されることを意図してのことです。SSD搭載機では問題のない処理ですが、HDD搭載機では若干遅く感じるかもしれません(もはやHDD搭載機が身の回りにないので不明)。

これまでは、マンガの新規連載がはじまるとcurlコマンドで画像をダウンロードしてPDFに連結する作業を手で行なっていたのですが(誰も頼んでねえよ)、新規連載が増えたので自動化してみました。それでもありあわせの部品を組み合わせただけなので、それほど手間はかかっていません。

本Scriptとは別に更新された差分をPDFに連結するAppleScriptを作って日々実行し、大きな画面でブラウズするのに役立てています。割とこういう、ごくごく私的なScriptで野心的な処理を先行してテストしているものです。

AppleScript名:ツイ4のページで新規連載マンガの画像を取得してPDF化(新規連載のPDF化)v3
– Created 2016-09-05 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “AppKit”
use framework “QuartzCore”
use BridgePlus : script “BridgePlus”
–http://piyocast.com/as/archives/4808

property SMSForder : a reference to current application’s SMSForder
property |NSURL| : a reference to current application’s |NSURL|
property NSURLRequest : a reference to current application’s NSURLRequest
property NSURLConnection : a reference to current application’s NSURLConnection
property NSArray : a reference to current application’s NSArray
property NSFileManager : a reference to current application’s NSFileManager
property NSNumberFormatter : a reference to current application’s NSNumberFormatter
property NSPredicate : a reference to current application’s NSPredicate
property PDFPage : a reference to current application’s PDFPage
property PDFDocument : a reference to current application’s PDFDocument
property NSURLRequestUseProtocolCachePolicy : a reference to current application’s NSURLRequestUseProtocolCachePolicy
property NSNumberFormatterPadBeforePrefix : a reference to current application’s NSNumberFormatterPadBeforePrefix
property NSImage : a reference to current application’s NSImage
property NSSortDescriptor : a reference to current application’s NSSortDescriptor
property NSNumber : a reference to current application’s NSNumber
property NSRegularExpressionDotMatchesLineSeparators : a reference to current application’s NSRegularExpressionDotMatchesLineSeparators
property NSRegularExpressionAnchorsMatchLines : a reference to current application’s NSRegularExpressionAnchorsMatchLines
property NSRegularExpression : a reference to current application’s NSRegularExpression
property NSString : a reference to current application’s NSString

property theTargetSite : “http://sai-zen-sen.jp/”

tell application “Safari”
  if (count every document) = 0 then
    display notification “Safari does not open web page”
    
return
  end if
  
  
set docTitle to (do JavaScript “document.title” in front document) –Title
  
  
tell front document –URL
    set aURL to URL
  end tell
end tell

if aURL does not start with theTargetSite then
  display notification “This site is not the target”
  
return
end if

–Safariの最前面のウィンドウから画像リンクをすべて取得(Height, Width, URL)
set aList to getImageSizeAndURLOfFrontSafariDocument() of me

–取得した画像情報の2D Listをサイズで降順ソート
load framework –Force loading BridgePlus framework
set sortIndexes to {0, 1} –Key Item id: begin from 0
set sortOrders to {false, false}
set sortTypes to {“compare:”, “compare:”}
set resList to (current application’s SMSForder’s subarraysIn:(aList) sortedByIndexes:sortIndexes ascending:sortOrders sortTypes:sortTypes |error|:(missing value))

–画像が取得できなかったら処理終了
if (resList as list) = {} then
  display notification “There is no images on this page”
  
return –No Result
end if

–最大サイズの画像情報を取得する(おそらくマンガ)
set {maxHeight, maxWidth, maxURL} to contents of first item of (resList as list)

set aNSURL to |NSURL|’s URLWithString:maxURL
set aNSURLfilename to (aNSURL’s lastPathComponent())
set aNSURLpure to aNSURL’s URLByDeletingLastPathComponent()
set aNSURLextension to aNSURLfilename’s pathExtension() as string
set aNSURLfilenameLen to (aNSURLfilename’s stringByDeletingPathExtension())’s |length|() as integer –画像ファイル名から拡張子を除去した部分の文字列長

–画像情報リストを画像サイズで抽出
set maxHeightStr to (maxHeight as integer) as string
set maxWidthStr to (maxWidth as integer) as string
set thePred to NSPredicate’s predicateWithFormat:(“(self[0] == “ & maxHeightStr & “) AND (self[1] == “ & maxWidthStr & “)”)
set bArray to (resList’s filteredArrayUsingPredicate:thePred) as list

–URLからファイル名の数値部分のみ抽出
set imageArray to current application’s NSMutableArray’s new()
repeat with i in bArray
  set j to contents of last item of i –(Image URL)
  
set aTmpURL to (|NSURL|’s URLWithString:j)
  
set aTmpfilename to (aTmpURL’s lastPathComponent()) as string
  
set numStr to first item of (my findPattern:(“^\\d{1,” & (aNSURLfilenameLen as string) & “}”) inString:aTmpfilename)
  
set jj2 to (SMSForder’s transformedFrom:numStr ICUTransform:“Fullwidth-Halfwidth” inverse:false) as integer
  (
imageArray’s addObject:jj2)
end repeat

–ファイル名から抽出した数値の最小値と最大値を求める。ただ、実運用したらWeb側から画像をすべて取得されない(非同期読み込みを行なっているらしい)ケースがあったため、ここの値は参考値程度にしか使えなかった
set maxRes to (imageArray’s valueForKeyPath:“@max.self”)’s intValue() –最大値
set minRes to (imageArray’s valueForKeyPath:“@min.self”)’s intValue() –最小値
log {minRes, maxRes}

–PDFのファイル名と場所をユーザーに確認
set pdfFile to (choose file name with prompt “Select PDF Name & Location” default location (path to pictures folder) default name (docTitle & “.pdf”))
set pdfFilePOSIX to POSIX path of pdfFile
set newFilePath to current application’s NSString’s stringWithString:pdfFilePOSIX

–Make Blank PDF
set aPDFdoc to PDFDocument’s alloc()’s init()

–Download each image and append to blank PDF
set insCount to 1 –画像ダウンロード用のページ数(Loop Counter)とPDF連結用のページ番号(insCount)を分離

–repeat with i from minRes as integer to maxRes as integer
repeat with i from 1 to 9999
  –URL部品の連結
  
set aFILENAME to numToZeroPaddingStr(i, aNSURLfilenameLen, “0″) of me
  
set aFULLURL to (aNSURLpure’s absoluteString() as string) & (aFILENAME as string) & “.” & (aNSURLextension as string)
  
set aURL to (|NSURL|’s URLWithString:aFULLURL)
  
  
–URL(画像)をダウンロード
  
set {uRes, headerRes, aData} to checkURLResourceExistence(aURL, 3) of me
  
  
if uRes = true then
    display notification “Episode “ & (i as string) & ” exists…”
    
set bImage to (NSImage’s alloc()’s initWithData:aData)
    (
aPDFdoc’s insertPage:(PDFPage’s alloc()’s initWithImage:bImage) atIndex:(insCount - 1))
    (
aPDFdoc’s writeToFile:newFilePath) –1Page更新するたびにファイル保存
    
set changedF to true –PDFにページが追記されたことを検出
  else
    display notification “No more new episode….”
    
exit repeat
  end if
  
  
set insCount to insCount + 1
end repeat

–FinderコメントにURLを記入
tell application “Finder”
  set comment of (pdfFile as alias) to (aNSURLpure’s absoluteString() as string)
end tell

–生成したPDFをオープン。ビューワー経由ではなくFinder経由でopen命令を送って表示
tell application “Finder”
  open (pdfFile as alias)
end tell
–ここで処理終了

—————

on getImageSizeAndURLOfFrontSafariDocument()
  set aList to {}
  
  
tell application “Safari”
    if its running then
      if (count every document) = 0 then return {}
      
set aRes to (do JavaScript “document.images.length” in front document)
      
      
repeat with i from 0 to (aRes - 1)
        set aHeight to do JavaScript ((“document.images[” & i as string) & “].height”) in front document
        
set aWidth to do JavaScript ((“document.images[” & i as string) & “].width”) in front document
        
set aSRC to do JavaScript ((“document.images[” & i as string) & “].src”) in front document
        
set the end of aList to {aHeight, aWidth, aSRC}
      end repeat
    end if
  end tell
  
  
return aList
end getImageSizeAndURLOfFrontSafariDocument

on findPattern:thePattern inString:theString
  set theOptions to ((NSRegularExpressionDotMatchesLineSeparators) as integer) + ((NSRegularExpressionAnchorsMatchLines) as integer)
  
set theRegEx to NSRegularExpression’s regularExpressionWithPattern:thePattern options:theOptions |error|:(missing value)
  
set theFinds to theRegEx’s matchesInString:theString options:0 range:{location:0, |length|:length of theString}
  
set theFinds to theFinds as list
  
set theResult to {}
  
set theNSString to NSString’s stringWithString:theString
  
  
repeat with i in theFinds
    set theRange to (contents of i)’s range()
    
set end of theResult to (theNSString’s substringWithRange:theRange) as string
  end repeat
  
return theResult
end findPattern:inString:

–1D List(文字)をsort / ascOrderがtrueだと昇順ソート、falseだと降順ソート
on sort1DList:theList ascOrder:aBool
  set aDdesc to NSSortDescriptor’s sortDescriptorWithKey:“self” ascending:aBool selector:“localizedCaseInsensitiveCompare:”
  
set theArray to NSArray’s arrayWithArray:theList
  
return (theArray’s sortedArrayUsingDescriptors:{aDdesc}) as list
end sort1DList:ascOrder:

–整数の値に指定桁数ゼロパディングして文字列で返す
on numToZeroPaddingStr(aNum as integer, aDigit as integer, paddingChar as text)
  set aNumForm to NSNumberFormatter’s alloc()’s init()
  
aNumForm’s setPaddingPosition:(NSNumberFormatterPadBeforePrefix)
  
aNumForm’s setPaddingCharacter:paddingChar
  
aNumForm’s setMinimumIntegerDigits:aDigit
  
  
set bNum to NSNumber’s numberWithInt:aNum
  
set aStr to aNumForm’s stringFromNumber:bNum
  
  
return aStr as text
end numToZeroPaddingStr

– 指定URLにファイル(画像など)が存在するかチェック
–> {存在確認結果(boolean), レスポンスヘッダー(NSDictionary), データ(NSData)}
on checkURLResourceExistence(aURL, timeOutSec as real)
  set aRequest to (NSURLRequest’s requestWithURL:aURL cachePolicy:(NSURLRequestUseProtocolCachePolicy) timeoutInterval:timeOutSec)
  
set aRes to (NSURLConnection’s sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value))
  
set dRes to (first item of (aRes as list))
  
set bRes to (second item of (aRes as list))
  
if bRes is not equal to missing value then
    set hRes to (bRes’s allHeaderFields())
    
set aResCode to (bRes’s statusCode()) as integer
  else
    set hRes to {}
    
set aResCode to 404
  end if
  
return {(aResCode = 200), hRes, dRes}
end checkURLResourceExistence

–指定PDFのページ数をかぞえる(10.9対応。普通にPDFpageから取得)
–返り値:PDFファイルのページ数(整数値)
on pdfPageCount(aFile)
  set aFile to POSIX path of aFile
  
set theURL to |NSURL|’s fileURLWithPath:aFile
  
set aPDFdoc to PDFDocument’s alloc()’s initWithURL:theURL
  
set aRes to aPDFdoc’s pageCount()
  
return aRes as integer
end pdfPageCount

★Click Here to Open This Script 

2017/01/07 iTunesライブラリの曲のアーティスト名を集計

iTunesライブラリ中に入っている「曲」(song)のアーティスト名を集計するAppleScriptです。

iTunesLibraryフレームワークを用いてライブラリにアクセスしているので、iTunes.appが起動している必要はありません。

アーティスト名については、ライブラリ中にかなりイレギュラーなデータが存在しているため、対策が必要でした。

・初期のiTunesで、CDからリッピングしたものの、CDDBに登録がなかったため、アーティスト名などが登録されていない→ エラートラップで対処

・アーティスト名で姓(last name)と名(first name)の間に空白が入っていたりいなかったりするなど、表記ゆらぎが存在していたため、ゆらぎを吸収

もう少し高速に実行できてもよさそうですが、ライブラリ中のtrackが8,100程度のときに、10回実行時の平均は2.8秒程度です(MacBook Pro Retina 2012)。

AppleScript名:iTunesライブラリの曲のアーティスト名を集計
– Created 2017-01-07 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “iTunesLibrary”
–http://piyocast.com/as/archives/4379

set library to current application’s ITLibrary’s libraryWithAPIVersion:“1.0″ |error|:(missing value)
if library is equal to missing value then return

set allTracks to library’s allMediaItems()
set allCount to allTracks’s |count|()

set anEnu to allTracks’s objectEnumerator()
set newArray to current application’s NSMutableArray’s alloc()’s init()

repeat
  set aPL to anEnu’s nextObject()
  
if aPL = missing value then exit repeat
  
try
    set aKind to (aPL’s mediaKind) as integer
    
    
if (aKind as integer) is equal to 2 then –Music, Song
      set plName to aPL’s artist’s |name| as string
      
set pl2Name to (my changeThis:” “ toThat:“” inString:plName) –日本語アーティスト名で姓と名の間にスペースが入っているものがある(表記ゆらぎ)ので対策
      
newArray’s addObject:(pl2Name)
    end if
  on error
    set aLoc to (aPL’s location’s |path|()) as string
    
log aLoc
  end try
end repeat

set aRes to countItemsByItsAppearance(newArray) of me
–>  {{theName:”浜田省吾”, numberOfTimes:442}, {theName:”B’z”, numberOfTimes:379}, {theName:”渡辺岳夫・松山祐士”, numberOfTimes:199}, {theName:”VariousArtists”, numberOfTimes:192}, {theName:”菅野よう子”, numberOfTimes:108}, {theName:”布袋寅泰”, numberOfTimes:100}, {theName:”三枝成彰”, numberOfTimes:95}, {theName:”宇多田ヒカル”, numberOfTimes:94}, {theName:”宮川泰”, numberOfTimes:81}, {theName:”MichaelJackson”, numberOfTimes:78}, {theName:”稲葉浩志”, numberOfTimes:73}, …

–出現回数で集計
on countItemsByItsAppearance(aList)
  set aSet to current application’s NSCountedSet’s alloc()’s initWithArray:aList
  
set bArray to current application’s NSMutableArray’s array()
  
set theEnumerator to aSet’s objectEnumerator()
  
  
repeat
    set aValue to theEnumerator’s nextObject()
    
if aValue is missing value then exit repeat
    
bArray’s addObject:(current application’s NSDictionary’s dictionaryWithObjects:{aValue, (aSet’s countForObject:aValue)} forKeys:{“theName”, “numberOfTimes”})
  end repeat
  
  
–出現回数(numberOfTimes)で降順ソート
  
set theDesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:“numberOfTimes” ascending:false
  
bArray’s sortUsingDescriptors:{theDesc}
  
  
return bArray as list
end countItemsByItsAppearance

on changeThis:findString toThat:repString inString:someText
  set theString to current application’s NSString’s stringWithString:someText
  
set theString to theString’s stringByReplacingOccurrencesOfString:findString withString:repString options:(current application’s NSRegularExpressionSearch) range:{location:0, |length|:length of someText}
  
return theString as text
end changeThis:toThat:inString:

★Click Here to Open This Script 

2016/12/14 レコードのうち、最大の値を持つKeyを返す

レコードのうち、最大の値を持つキーを返すAppleScriptです。

AppleScriptからMicrosoftの表情認識APIを呼び出せるようになったはいいものの、得られたデータからどの表情の可能性が高いかを判定する必要があり、そのために書いてみたものです。

Cocoaの機能を用いて、レコードのキーを取り出したり、さまざまな処理を行っています。こうした処理が手軽にできるようになったのは、実にいいことです。

さらに、画像から顔認識を行い、表情を読み取ってデータ化できるというのは、実に素晴らしいと思うものです。

ただ、いろいろ実験を行ってみたところ・・・恐れとか悲しみとか軽蔑といった微妙な感情については検出しづらいことがわかってきました。最大の値のものをピックアップするという方法ではなく、微妙な感情については別途評価を行う必要がありそうです。

mona_lisa_by_leonardo_da_vinci_from_c2rmf_retouched-1.jpg
–> “neutral”

donaldtrump61815_resized.png
–> “anger”

AppleScript名:レコードのうち、最大の値を持つKeyを返す
– Created 2016-12-14 by Takaaki Naganoya
– 2016 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4359

set aDict to current application’s NSDictionary’s dictionaryWithDictionary:{sadness:0.01133416, anger:1.03975857E-4, happiness:2.90919736E-4, fear:2.28432211E-4, neutral:0.9830475, contempt:2.4698046E-4, disgust:1.02946949E-4, surprise:0.00464509334}
set aMacKey to retMaxValueKey(aDict) of me
–>  ”neutral”

on retMaxValueKey(aDict)
  set aValList to aDict’s allValues()
  
set maxVal to (sort1DNumList(aValList, false) of me)’s firstObject()
  
set keyList to (aDict’s allKeys()) as list
  
  
repeat with i in keyList
    set j to contents of i
    
set aTmp to (aDict’s valueForKey:j)
    
set aRes to compareNumerically(maxVal, aTmp) of me
    
if aRes = true then return j
  end repeat
  
  
return false
end retMaxValueKey

–Numerical Strings Compare
on compareNumerically(aText as text, bText as text)
  set aStr to current application’s NSString’s stringWithString:aText
  
return (aStr’s compare:bText options:(current application’s NSNumericSearch)) = current application’s NSOrderedSame
end compareNumerically

–1D List(数値)をsort / ascOrderがtrueだと昇順ソート、falseだと降順ソート
on sort1DNumList(theList, aBool)
  set theSet to current application’s NSSet’s setWithArray:theList
  
set theDescriptor to current application’s NSSortDescriptor’s sortDescriptorWithKey:(missing value) ascending:aBool
  
set sortedList to theSet’s sortedArrayUsingDescriptors:{theDescriptor}
  
return (sortedList)
end sort1DNumList

★Click Here to Open This Script 

2016/09/20 連番JPEGファイルを読み込んで連結したPDFを作成(新規作成)

連番JPEG画像を番号順にソートして、順次連結したPDFを新規作成するAppleScriptです。

jpeg_catfiles.jpg

このような連番画像を連結して任意のファイル名のPDFに合成します。出来上がるPDFは、元のJPEGファイルの圧縮度に応じて大きくなります。

新規作成よりも、すでに存在しているPDFにJPEGファイルを連結するほうが実用的だと思います。

AppleScript名:連番JPEGファイルを読み込んで連結したPDFを作成(新規作成)
– Created 2016-09-20 by Takaaki Naganoya
– 2016 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “QuartzCore”
use framework “Quartz”
use framework “AppKit”

set aExt to “.jpg”
set aFol to choose folder
set fList to getFilePathList(aFol, aExt) of me
set f2List to my sort1DList:fList ascOrder:true –sort by ascending

set newFile to POSIX path of (choose file name with prompt “新規PDFファイルの名称を選択”)
set newFilePath to current application’s NSString’s stringWithString:newFile

–Make Blank PDF
set aPDFdoc to current application’s PDFDocument’s alloc()’s init()

set pageNum to 0

repeat with i in f2List
  set j to contents of i
  
set aURL to (current application’s |NSURL|’s fileURLWithPath:j)
  
set bImg to (current application’s NSImage’s alloc()’s initWithContentsOfURL:aURL)
  (
aPDFdoc’s insertPage:(current application’s PDFPage’s alloc()’s initWithImage:bImg) atIndex:pageNum)
  
set pageNum to pageNum + 1
end repeat

aPDFdoc’s writeToFile:newFilePath

–ASOCで指定フォルダのファイルパス一覧取得(拡張子指定つき)
on getFilePathList(aFol, aExt)
  set aPath to current application’s NSString’s stringWithString:(POSIX path of aFol)
  
set aFM to current application’s NSFileManager’s defaultManager()
  
set nameList to (aFM’s contentsOfDirectoryAtPath:aPath |error|:(missing value)) as list
  
set anArray to current application’s NSMutableArray’s alloc()’s init()
  
  
repeat with i in nameList
    set j to i as text
    
if (j ends with aExt) and (j does not start with “.”) then –exclude invisible files
      set newPath to (aPath’s stringByAppendingString:j)
      (
anArray’s addObject:newPath)
    end if
  end repeat
  
  
return anArray as list
end getFilePathList

–1D List(文字)をsort / ascOrderがtrueだと昇順ソート、falseだと降順ソート
on sort1DList:theList ascOrder:aBool
  set aDdesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:“self” ascending:aBool selector:“localizedCaseInsensitiveCompare:”
  
set theArray to current application’s NSArray’s arrayWithArray:theList
  
return (theArray’s sortedArrayUsingDescriptors:{aDdesc}) as list
end sort1DList:ascOrder:

★Click Here to Open This Script 

2016/07/28 Bookフォルダリネーム

書籍の原稿管理用のAppleScriptです。原稿は章ごとにフォルダ分けしており、

 9999 フォルダ名あるいはファイル名

といったように、仮想ノンブルを前に付けています。この仮想ノンブルで前後関係を制御しており、基本的に数字の大小だけが重要です。

whole.png

最後に1冊分PDFにまとめてビルドするAppleScriptで、書き出し対象のフォルダ以下のMarkdownファイルとPagesのファイルをピックアップし、ファイル名でソート。ソート順は仮想ノンブルによってコントロールされることになります。

Finderの並べ替えを使っているので、シンプルでわかりやすく、使い勝手もよいのですが……ただひとつ難点が。

途中にコンテンツを挿入したい場合にも仮想ノンブルでコントロールしつつ、途中に入るように操作するのですが……場合によってはフォルダおよび書類の仮想ノンブル部分だけリネームする必要が出てきます。

とくに、目下改定作業中の「AppleScript最新リファレンス」のコマンドリファレンス部分は、初版から大幅に改定・補充を行っているため、ひんぱんにリネームが発生して大変です(ーー;;

そこで、こんなScriptを組んでリネーム作業だけ自動化してみました。

Finder上でフォルダを選択して本Scriptを実行すると、選択中のフォルダ名から仮想ノンブルの番号を取得し、

before2.png

内包するフォルダに入っているファイルについても、すべて仮想ノンブル部分を書き換えます。

after2.png

AppleScript名:Bookフォルダリネーム
– Created 2016-07-28 by Takaaki Naganoya
– 2016 Piyomaru Software
use AppleScript version “2.4″
use framework “Foundation”
use scripting additions
# http://piyocast.com/as/archives/4177

set aStep to 10

tell application “Finder”
  set aSel to (selection as alias list)
end tell
if aSel = {} or aSel = “” then return
set aFol to first item of aSel

tell application “Finder”
  –選択中のフォルダの名称を取得
  
set aName to name of aFol
  
  
–選択中のフォルダに入っている書類を取得
  
tell folder (aFol as string)
    set fList to name of every file as alias list
  end tell
end tell

set {startNumStr, folderNameStr} to parseNumAndString(aName) of me
set startNum to startNumStr as integer

set aRes to sort1DListOrder(fList, true) of me –昇順でファイル名をソート

set aFolStr to aFol as string
set aCount to startNumStr
tell application “Finder”
  repeat with i in aRes
    set anAlias to (aFolStr & (i as string)) as alias
    
set tmpName to name of anAlias
    
    
set {curNum, curName} to parseNumAndString(tmpName) of me
    
set newName to (retZeroPaddingText(aCount, length of curNum) of me) & ” “ & curName
    
    
set name of anAlias to newName
    
    
set aCount to aCount + aStep
  end repeat
end tell

–文字列から先頭にあるとおぼしき数値と、スペースで区切られた後続の文字列を分離する
on parseNumAndString(aName)
  set aOffset to offset of ” “ in aName
  
if aOffset = 0 then error “Illigal Format”
  
set aNum to text 1 thru (aOffset - 1) of aName
  
set aText to text (aOffset + 1) thru -1 of aName
  
return {aNum, aText}
end parseNumAndString

–1D Listをsort / ascOrderがtrueだと昇順ソート、falseだと降順ソート
on sort1DListOrder(theList, aBool)
  set aDdesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:“self” ascending:aBool selector:“localizedCaseInsensitiveCompare:”
  
set theArray to current application’s NSArray’s arrayWithArray:theList
  
return (theArray’s sortedArrayUsingDescriptors:{aDdesc}) as list
end sort1DListOrder

on retZeroPaddingText(aNum as integer, aDigitNum as integer)
  if aNum > (((10 ^ aDigitNum) as integer) - 1) then return “” –Range Check
  
set aFormatter to current application’s NSNumberFormatter’s alloc()’s init()
  
aFormatter’s setUsesGroupingSeparator:false
  
aFormatter’s setAllowsFloats:false
  
aFormatter’s setMaximumIntegerDigits:aDigitNum
  
aFormatter’s setMinimumIntegerDigits:aDigitNum
  
aFormatter’s setPaddingCharacter:“0″
  
set aStr to aFormatter’s stringFromNumber:(current application’s NSNumber’s numberWithFloat:aNum)
  
return aStr as string
end retZeroPaddingText

★Click Here to Open This Script 

2016/06/18 指定フォルダ以下にあるMacDownとPages書類をソートしてPDFに書き出す

指定フォルダ以下にあるMacDownで記述したMarkdown書類と、Pages書類をすべての階層からピックアップしてファイル名でソートして、すべてデスクトップにPDFで書き出すAppleScriptです。

コンパイル(構文確認)および実行に際しては、Shane StanleyのScript Library「Bridge Plus」をインストールしておく必要があります。また、GUI Scriptingを利用しているため「システム環境設定」>「セキュリティとプライバシー」>「プライバシー」>「アクセシビリティ」でスクリプトエディタ(アプレットとして実行する場合にはアプレットそのもの)を登録して許可しておく必要があります。

「技術書典」に出す電子ブックのフォーマットがギリギリまで決まらず、しかも縦長のスクロールさせるタイプのものにできないかとあがいていたのですが、結局iPadあたりで読むことを考慮するとiPadのリーダーの仕様にしたがう必要があります。

……あれ?(^ー^; 結局、ページめくりは発生するし、一般的な本と同じような体裁になってしまいますよ → フォーマットがPDFになりました。

Markdown書類とPages書類が混在しているフォルダ構造のトップ階層のフォルダを指定すると、Markdown書類とPages書類をピックアップし、ファイル名でそれらをソートし、順次PDFに書き出すAppleScriptを書いてみました(必要は発明のマザー!)。

book1.png

ただ、MacDownには「書類をPDFに書き出す」という機能がAppleScript側に公開されていません(T_T)。

macdown_dict.png

ないものを「ないない」と嘆いても仕方がないので、さっさとGUI Scriptingで強制的にメニュー操作することにしました。

macdown_gui.png

で、どこに? どこに保存させるのでしょう??

大丈夫! そんなときには、保存ダイアログで幾つかのフォルダに強制的に移動させるキーボードショートカットが存在しており、Command-Dは「カレントディレクトリをデスクトップに移動させる」=「保存先をデスクトップにする」働きをします。

このため、保存先を操作しづらい(不可能とはいいませんけれども)GUI Scriptingにおいて保存先を指定することが、デスクトップフォルダについては可能になっています。

Pagesの方はひじょうに素直に(GUI Scriptingなんて使わずに)PDF書き出しが可能です。

そんなわけで、時間に追い詰められながらもなんとか大量のデータ処理を行っているのでありました。書き出した大量のPDFもAppleScriptでさくっと連結できるので、非常にいい感じです。あとは、本が完成すれば、、、、

AppleScript名:指定フォルダ以下にあるMDとPagesをソートしてPDFに書き出す
– Created 2016-06-17 by Takaaki Naganoya
– 2016 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use BridgePlus : script “BridgePlus”
–use spotLight : script “spotlightLib”

property searchRes : {}

load framework

set origPath to POSIX path of (choose folder with prompt “Markdown/Pages ファイルの入っているフォルダを選択”)

set aResList to (spotlightSearch(origPath, “kMDItemKind == ’Markdown’ || kMDItemKind == ’Pages 一般書類’”) of me) as list –Caution! this parameter is *localized*

–フルパスとファイル名のペアの2D Listを作成
set newList to {}
repeat with i in aResList
  set j to contents of i
  
set aStr to (current application’s NSString’s stringWithString:j)
  
set aFileName to aStr’s lastPathComponent()
  
set the end of newList to {aStr, aFileName}
end repeat

–番号順にソート
set sortIndexes to {1} –Key Item id: begin from 0, Sort by filename
set sortOrders to {true}
set sortTypes to {“compare:”}
set resList to (current application’s SMSForder’s subarraysIn:(newList) sortedByIndexes:sortIndexes ascending:sortOrders sortTypes:sortTypes |error|:(missing value)) as list
if resList = {} then return –No Result

–ソートした順番にMarkdownファイル/Pages書類ファイルをオープンしてデスクトップにPDF生成してクローズ
repeat with i in newList
  copy i to {fullPath, aFileName}
  
  
set apFile to (POSIX file (fullPath as string))
  
set anAlias to apFile as alias
  
set aFileName to aFileName as string
  
  
if aFileName ends with “.md” then
    exportFromMacDown(anAlias) of me –Markdown
  else if aFileName ends with “.pages” then
    exportFromPages(anAlias) of me –Pages
  end if
end repeat

–指定のPagesファイル(alias)をデスクトップ上にPDFで書き出し
on exportFromPages(anAlias)
  tell application “Finder”
    set aName to name of anAlias
  end tell
  
  
set dtPath to (path to desktop) as string
  
set outPath to dtPath & aName & “.pdf”
  
  
tell application “Pages”
    close every document without saving
    
open anAlias
    
export document 1 to file outPath as PDF with properties {image quality:Best}
    
close every document without saving
  end tell
end exportFromPages

–指定のMacDownファイル(alias)をデスクトップ上にPDFで書き出し
on exportFromMacDown(anAlias)
  tell application “MacDown”
    open {anAlias}
  end tell
  
  
tell current application
    delay 1 –ここの時間待ちが少ないと画像抜けが発生?
  end tell
  
macDownForceSave() of me
  
  
tell application “MacDown”
    close every document without saving
  end tell
end exportFromMacDown

–注意!! ここでGUI Scriptingを使用。バージョンが変わったときにメニュー階層などの変更があったら書き換え
on macDownForceSave()
  activate application “MacDown”
  
tell application “System Events”
    tell process “MacDown”
      – File > Export > PDF
      
click menu item 2 of menu 1 of menu item 14 of menu 1 of menu bar item 3 of menu bar 1
      
      
–Go to Desktop Folder
      
keystroke “d” using {command down}
      
      
–Save Button on Sheet
      
click button 1 of sheet 1 of window 1
    end tell
  end tell
end macDownForceSave

–Spotlight Libの内容を引っ張り出してきた
on spotlightSearch(origPOSIXpath, aCondition)
  
  
set searchRes to {} –initialize
  
  
initiateSearchForFullPath(aCondition, origPOSIXpath) –Predicate & Scope Directory
  
  
–Waiting for the result
  
repeat while searchRes = {}
    current application’s NSThread’s sleepForTimeInterval:(“0.001″ as real) –delay 0.001
  end repeat
  
  
set anObj to searchRes’s firstObject() –Pick up the first one for test
  
if anObj = missing value then return {} –No Result
  
  
–set anAttrList to anObj’s attributes() –”mdls” attributes
  
–>  (NSArray) {”kMDItemContentTypeTree”, “kMDItemContentType”, “kMDItemPhysicalSize”, …}
  
  
set resArray to {}
  
repeat with anItem in my searchRes
    set j to contents of anItem
    
set aPath to (j’s valueForAttribute:“kMDItemPath”) as string
    
set the end of resArray to aPath
  end repeat
  
  
return resArray
end spotlightSearch

on initiateSearchForFullPath(aQueryStrings, origPath)
  
  
set aSearch to current application’s NSMetadataQuery’s alloc()’s init()
  
  
current application’s NSNotificationCenter’s defaultCenter()’s addObserver:(me) selector:“queryDidUpdate:” |name|:(current application’s NSMetadataQueryDidUpdateNotification) object:aSearch
  
current application’s NSNotificationCenter’s defaultCenter()’s addObserver:(me) selector:“initalGatherComplete:” |name|:(current application’s NSMetadataQueryDidFinishGatheringNotification) object:aSearch
  
  
set aPredicate to current application’s NSPredicate’s predicateWithFormat:aQueryStrings
  
aSearch’s setPredicate:aPredicate
  
  
set aScope to current application’s NSArray’s arrayWithObjects:{origPath}
  
aSearch’s setSearchScopes:aScope
  
  
set sortKeys to current application’s NSSortDescriptor’s sortDescriptorWithKey:“kMDItemFSName” ascending:true
  
aSearch’s setSortDescriptors:(current application’s NSArray’s arrayWithObject:sortKeys)
  
  
aSearch’s startQuery()
  
end initiateSearchForFullPath

on queryDidUpdate:sender
  –  
end queryDidUpdate:

on initalGatherComplete:sender
  set anObject to sender’s object
  
anObject’s stopQuery()
  
current application’s NSNotificationCenter’s defaultCenter()’s removeObserver:me |name|:(current application’s NSMetadataQueryDidUpdateNotification) object:anObject
  
current application’s NSNotificationCenter’s defaultCenter()’s removeObserver:me |name|:(current application’s NSMetadataQueryDidFinishGatheringNotification) object:anObject
  
set my searchRes to anObject’s results()
end initalGatherComplete:

★Click Here to Open This Script 

2016/02/28 Safariの履歴を読んでトップ10をテキストで出力する

SafariのHistory.plistを読み込んで、アクセス回数のトップ10をテキスト出力するAppleScriptです。

Safari 9.1+OS X 10.11環境を前提としています。plistの内容については、Safariのバージョンごとに変更が加わってもおかしくないので、メジャーバージョンアップ時には動作確認する必要があることでしょう。

他のアプリケーションの.plistファイルにアクセスするのはかなり野蛮な処理ですが(Sandbox環境では許可されない)、場合によっては有用なこともあるでしょう。

ちなみに、Scriptの実行例はサブマシン(MacBook Air 2011)で実行したものです。

AppleScript名:Safariの履歴を読んでトップ10をテキストで出力する
– Created 2016-02-27 by Takaaki Naganoya
– Piyomaru Software 2016
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”

set libPath to (POSIX path of (path to library folder from user domain)) & “Safari/History.plist”
set aRec to retDictFromPlist(libPath) of me
set bRecList to aRec’s valueForKeyPath:“WebHistoryDates”

set bArray to sortRecListByLabel(bRecList, “visitCount”, false) of me
set outStr to “”
set aCount to 0

repeat with i from 0 to 100
  set anItem to (bArray’s objectAtIndex:i)
  
  
set aURL to (anItem’s valueForKeyPath:“”) as string
  
set aTitle to (anItem’s valueForKeyPath:“title”) as string
  
set aDispTItle to (anItem’s valueForKeyPath:“displayTitle”) as string
  
set aVisitCount to (anItem’s valueForKeyPath:“visitCount”) as string
  
set redirectURLs to (anItem’s valueForKeyPath:“redirectURLs”) as list
  
set lastVisitedDate to (anItem’s valueForKeyPath:“lastVisitedDate”) as string
  
  
if aURL does not start with “http://piyocast.com” then –自分のところのサイト関連を除外
    set aStr to ((aCount + 1) as string) & “:(” & (aVisitCount as string) & “)” & aTitle & return
    
set outStr to outStr & aStr
    
    
set aCount to aCount + 1
    
if aCount = 10 then exit repeat
    
  end if
  
  
end repeat

return outStr

(*
–>  ”1:(7657)radiko.jp
2:(608)Apple
3:(416)ソフトバンクWi-Fiスポット
4:(156)公式Apple Store(日本)- iPad Air、MacBook Pro、ほかにもたくさん
5:(125)Yahoo! JAPAN
6:(108)Mac整備済製品 - Apple Store (Japan)
7:(54)Apple-Style \”アップルなBlogをリンク\”
8:(44)PC Watch
9:(31)Facebook
10:(28)アップル - Mac Pro

*)

–Read plist
on retDictFromPlist(aPath as text)
  set thePath to current application’s NSString’s stringWithString:aPath
  
set thePath to thePath’s stringByExpandingTildeInPath()
  
set theDict to current application’s NSDictionary’s dictionaryWithContentsOfFile:thePath
  
return theDict
end retDictFromPlist

–リストに入れたレコードを、指定の属性ラベルの値でソート
on sortRecListByLabel(aRecList, aLabelStr, ascendF)
  set aArray to current application’s NSArray’s arrayWithArray:aRecList
  
set sortDesc to current application’s NSSortDescriptor’s alloc()’s initWithKey:aLabelStr ascending:ascendF
  
set sortDescArray to current application’s NSArray’s arrayWithObjects:sortDesc
  
set sortedArray to aArray’s sortedArrayUsingDescriptors:sortDescArray
  
return sortedArray
end sortRecListByLabel

★Click Here to Open This Script 

2014/12/25 アプリケーションのローカライズ分布を取得する v5

/Applicationsフォルダ内に入っているアプリケーションのローカライズ言語の度数分布を取得するAppleScriptのバージョンアップ版です。

1つのアプリケーションが複数の重複するローカライズ情報を持っているケース({”de”, “English”, “fr”, “French”, “German”, “ja”, “Japanese”})があり、1つのアプリケーションから情報を取得する段階で重複を除去するルーチンを作成。重複を排除して({”German”, “English”, “French”,”Japanese”})のように整理して取得するように変更しました。

また、/Applicationsフォルダの直下のアプリケーションのファイルしか取得していなかったので、mdfindで/Applicationsフォルダ以下すべてのアプリケーションを取得するように変更。

また、言語名ではなく出現頻度でソートするように変更しました。

途中で、mdfindでアプリケーションのファイルを抽出する処理を試していたら、サブフォルダ内のアプリケーションが出てこないという問題があり・・・開発マシンのSSD内のspotlightの検索辞書が壊れていたようで、再生成したら正常に動作するようになりました。mdfindの処理の落とし穴です。

マイナー言語(失礼!)をこまかくフォローしているのはGoogleアプリかと思っていたら、VLCでした。OSがサポートしていない言語用のローカライズを行っても意味はないのですが、ムービーの字幕表示などの関係でしょうか?

AppleScript名:アプリケーションのローカライズ分布を取得する v5
– Created 2014-12-23 by Takaaki Naganoya
– Changed 2014-12-24 by Shane Stanley
– Changed 2014-12-25 by Takaaki Naganoya

use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “ASObjCExtras”

–mdfindで指定フォルダ以下にあるすべてのアプリケーションファイルを取得
set apPath1 to (path to applications folder) as string
set apList to getFileListWithSpotLight(“kMDItemContentType”, “com.apple.application-bundle”, apPath1) of me

set theCountedSet to current application’s NSCountedSet’s |set|()

–各アプリケーションのローカリゼーション情報を取得する
repeat with i in apList
  set j to contents of i
  
  
–指定アプリケーションのローカライズ言語を取得。重複を除去
  
set locList to getSpecifiedAppFilesLocalizationList(j) of me
  
–> {”de”, “English”, “fr”, “French”, “German”, “ja”, “Japanese”}
  
—-> {”German”, “English”, “French”,”Japanese”}
  
  
set locArray to (current application’s SMSFord’s Cocoaify:locList)
  
  
set theEnumerator to locArray’s objectEnumerator()
  
repeat
    set aValue to theEnumerator’s nextObject()
    
if aValue is missing value then exit repeat
    
    (
theCountedSet’s addObject:aValue)
  end repeat
end repeat

–NSCountedSetをNSMutableArrayに変換
set theArray to current application’s NSMutableArray’s array()
set theEnumerator to theCountedSet’s objectEnumerator()

–NSMutableArrayから順次取り出して、NSArrayに対してDictionaryを追加
repeat
  set aValue to theEnumerator’s nextObject()
  
if aValue is missing value then exit repeat
  
theArray’s addObject:(current application’s NSDictionary’s dictionaryWithObjects:{aValue, (theCountedSet’s countForObject:aValue)} forKeys:{“theName”, “numberOfTimes”})
end repeat

–出現回数(numberOfTimes)で降順ソート
set theDesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:“numberOfTimes” ascending:false
theArray’s sortUsingDescriptors:{theDesc}

return theArray as list

–> {{theName:”English”, numberOfTimes:733}, {theName:”Japanese”, numberOfTimes:404}, {theName:”French”, numberOfTimes:266}, {theName:”German”, numberOfTimes:265}, {theName:”Italian”, numberOfTimes:219}, {theName:”Spanish”, numberOfTimes:203}, {theName:”Dutch”, numberOfTimes:165}, {theName:”Chinese (China)”, numberOfTimes:149}, {theName:”Swedish”, numberOfTimes:133}, {theName:”Korean”, numberOfTimes:131}, {theName:”Russian”, numberOfTimes:129}, {theName:”Chinese (Taiwan)”, numberOfTimes:127}, {theName:”Portuguese”, numberOfTimes:124}, {theName:”Polish”, numberOfTimes:118}, {theName:”Danish”, numberOfTimes:117}, {theName:”Finnish”, numberOfTimes:110}, {theName:”Norwegian”, numberOfTimes:100}, {theName:”Czech”, numberOfTimes:92}, {theName:”Turkish”, numberOfTimes:90}, {theName:”Hungarian”, numberOfTimes:87}, {theName:”Greek”, numberOfTimes:76}, {theName:”Ukrainian”, numberOfTimes:75}, {theName:”Romanian”, numberOfTimes:74}, {theName:”Portuguese (Portugal)”, numberOfTimes:70}, {theName:”Slovak”, numberOfTimes:69}, {theName:”Catalan”, numberOfTimes:63}, {theName:”Croatian”, numberOfTimes:63}, {theName:”Arabic”, numberOfTimes:61}, {theName:”Thai”, numberOfTimes:61}, {theName:”Hebrew”, numberOfTimes:60}, {theName:”Indonesian”, numberOfTimes:58}, {theName:”Vietnamese”, numberOfTimes:57}, {theName:”Malay”, numberOfTimes:56}, {theName:”Spanish (Mexico)”, numberOfTimes:49}, {theName:”Base”, numberOfTimes:46}, {theName:”Portuguese (Brazil)”, numberOfTimes:22}, {theName:”Norwegian Bokmål”, numberOfTimes:21}, {theName:”Chinese (Simplified)”, numberOfTimes:19}, {theName:”Chinese”, numberOfTimes:14}, {theName:”English (United Kingdom)”, numberOfTimes:12}, {theName:”Bulgarian”, numberOfTimes:12}, {theName:”Chinese (Traditional)”, numberOfTimes:11}, {theName:”Latvian”, numberOfTimes:10}, {theName:”Lithuanian”, numberOfTimes:9}, {theName:”Slovenian”, numberOfTimes:9}, {theName:”Japanese (Japan)”, numberOfTimes:9}, {theName:”Czech (Czech Republic)”, numberOfTimes:9}, {theName:”Estonian”, numberOfTimes:8}, {theName:”Ukrainian (Ukraine)”, numberOfTimes:8}, {theName:”Spanish (Spain)”, numberOfTimes:7}, {theName:”Turkish (Turkey)”, numberOfTimes:7}, {theName:”Hungarian (Hungary)”, numberOfTimes:6}, {theName:”Danish (Denmark)”, numberOfTimes:6}, {theName:”Russian (Russia)”, numberOfTimes:6}, {theName:”French (XM)”, numberOfTimes:6}, {theName:”Swedish (Sweden)”, numberOfTimes:6}, {theName:”Serbian”, numberOfTimes:6}, {theName:”Norwegian Bokmål (Norway)”, numberOfTimes:6}, {theName:”Dutch (Netherlands)”, numberOfTimes:6}, {theName:”Persian”, numberOfTimes:5}, {theName:”Korean (South Korea)”, numberOfTimes:5}, {theName:”Finnish (Finland)”, numberOfTimes:5}, {theName:”Italian (Italy)”, numberOfTimes:5}, {theName:”German (Germany)”, numberOfTimes:5}, {theName:”French (Canada)”, numberOfTimes:5}, {theName:”French (France)”, numberOfTimes:5}, {theName:”Polish (Poland)”, numberOfTimes:5}, {theName:”Hindi”, numberOfTimes:5}, {theName:”English (United States)”, numberOfTimes:5}, {theName:”Hebrew (Israel)”, numberOfTimes:4}, {theName:”Arabic (United Arab Emirates)”, numberOfTimes:4}, {theName:”Spanish (Latin America)”, numberOfTimes:4}, {theName:”Romanian (Romania)”, numberOfTimes:4}, {theName:”Spanish (Namibia)”, numberOfTimes:4}, {theName:”Filipino”, numberOfTimes:4}, {theName:”Spanish (Laos)”, numberOfTimes:4}, {theName:”empty”, numberOfTimes:4}, {theName:”Greek (Greece)”, numberOfTimes:4}, {theName:”Telugu”, numberOfTimes:3}, {theName:”Malayalam”, numberOfTimes:3}, {theName:”Icelandic”, numberOfTimes:3}, {theName:”Kannada”, numberOfTimes:3}, {theName:”Galician”, numberOfTimes:3}, {theName:”Bengali”, numberOfTimes:3}, {theName:”Gujarati”, numberOfTimes:3}, {theName:”Marathi”, numberOfTimes:3}, {theName:”Tamil”, numberOfTimes:3}, {theName:”Belarusian”, numberOfTimes:3}, {theName:”Albanian”, numberOfTimes:2}, {theName:”Mongolian”, numberOfTimes:2}, {theName:”French (Morocco)”, numberOfTimes:2}, {theName:”Georgian”, numberOfTimes:2}, {theName:”Sinhala”, numberOfTimes:2}, {theName:”jp”, numberOfTimes:2}, {theName:”ua”, numberOfTimes:2}, {theName:”Amharic”, numberOfTimes:2}, {theName:”English (United Arab Emirates)”, numberOfTimes:2}, {theName:”Finnish (FL)”, numberOfTimes:2}, {theName:”Catalan (Spain)”, numberOfTimes:2}, {theName:”Basque”, numberOfTimes:2}, {theName:”English (Israel)”, numberOfTimes:2}, {theName:”Norwegian (NB)”, numberOfTimes:2}, {theName:”Oriya”, numberOfTimes:1}, {theName:”Punjabi”, numberOfTimes:1}, {theName:”Serbo-Croatian”, numberOfTimes:1}, {theName:”Walloon”, numberOfTimes:1}, {theName:”Corsican”, numberOfTimes:1}, {theName:”Sorani Kurdish”, numberOfTimes:1}, {theName:”Chinese (Hong Kong SAR China)”, numberOfTimes:1}, {theName:”Breton”, numberOfTimes:1}, {theName:”Cornish”, numberOfTimes:1}, {theName:”Chiga”, numberOfTimes:1}, {theName:”Aragonese”, numberOfTimes:1}, {theName:”Macedonian”, numberOfTimes:1}, {theName:”Japan”, numberOfTimes:1}, {theName:”Bosnian”, numberOfTimes:1}, {theName:”Ganda”, numberOfTimes:1}, {theName:”Tetum”, numberOfTimes:1}, {theName:”Welsh”, numberOfTimes:1}, {theName:”Armenian”, numberOfTimes:1}, {theName:”Kyrgyz”, numberOfTimes:1}, {theName:”Scottish Gaelic”, numberOfTimes:1}, {theName:”Afrikaans”, numberOfTimes:1}, {theName:”Zulu”, numberOfTimes:1}, {theName:”Swahili”, numberOfTimes:1}, {theName:”Burmese”, numberOfTimes:1}, {theName:”Chinese (Traditional, Taiwan)”, numberOfTimes:1}, {theName:”ct”, numberOfTimes:1}, {theName:”Tagalog”, numberOfTimes:1}, {theName:”Kazakh”, numberOfTimes:1}, {theName:”Bengali (India)”, numberOfTimes:1}, {theName:”Pashto”, numberOfTimes:1}, {theName:”Nepali”, numberOfTimes:1}, {theName:”Acoli”, numberOfTimes:1}, {theName:”Friulian”, numberOfTimes:1}, {theName:”Norwegian Nynorsk”, numberOfTimes:1}, {theName:”Asturian”, numberOfTimes:1}, {theName:”Thai (Thailand)”, numberOfTimes:1}, {theName:”Khmer”, numberOfTimes:1}, {theName:”Irish”, numberOfTimes:1}, {theName:”American”, numberOfTimes:1}, {theName:”Fulah”, numberOfTimes:1}, {theName:”Chinese (Traditional, Hong Kong SAR China)”, numberOfTimes:1}, {theName:”Occitan”, numberOfTimes:1}, {theName:”Interlingua”, numberOfTimes:1}, {theName:”Uzbek”, numberOfTimes:1}, {theName:”Azerbaijani”, numberOfTimes:1}}

–指定アプリケーションファイルの、指定Localeにおけるローカライズ言語リストを求める。重複を排除
on getSpecifiedAppFilesLocalizationList(anAppAlias as alias)
  
  
set aURL to (current application’s SMSFord’s URLFrom:anAppAlias)
  
set aBundle to current application’s NSBundle’s bundleWithURL:aURL
  
  
set theNSLocale to current application’s NSLocale’s localeWithLocaleIdentifier:“en” –Output Locale Name in English (en)
  
  
–Get Localization Info
  
set locList to aBundle’s localizations()
  
–> {”de”, “English”, “fr”, “French”, “German”, “ja”, “Japanese”}
  
set theEnumerator to locList’s objectEnumerator()
  
  
set theSet to current application’s NSMutableSet’s |set|()
  
  
repeat
    set aValue to theEnumerator’s nextObject()
    
if aValue is missing value then exit repeat
    
    
set theName to (theNSLocale’s displayNameForKey:(current application’s NSLocaleIdentifier) value:aValue)
    
    
if theName is missing value then
      –”English”や”French”などは、missing valueが返ってくるので、”English”や”French”などをそのまま追加
      
theSet’s addObject:aValue
    else
      –”de”や”fr”などは、”German”や”French”が返ってくるので、それを追加
      
theSet’s addObject:theName
    end if
  end repeat
  
  
–NSCountedSetをNSMutableArrayに変換
  
set theArray to current application’s NSMutableArray’s array()
  
set theEnumerator to theSet’s objectEnumerator()
  
  
–NSMutableArrayから順次取り出して、NSArrayに対してDictionaryを追加
  
repeat
    set aValue to theEnumerator’s nextObject()
    
if aValue is missing value then exit repeat
    
theArray’s addObject:aValue
  end repeat
  
  
return theArray as list
  
end getSpecifiedAppFilesLocalizationList

–指定階層下で、指定メタデータが指定パラメータであるファイルを取得(alias list)
on getFileListWithSpotLight(aMetaDataItem as string, aParam as string, startDir as {alias, string})
  set sDirText to quoted form of POSIX path of startDir
  
set shellText to “/usr/bin/mdfind ’” & aMetaDataItem & ” == \”" & aParam & “\”’ -onlyin “ & sDirText
  
try
    set aRes to do shell script shellText
  on error
    return {}
  end try
  
set pList to paragraphs of aRes
  
set aList to {}
  
repeat with i in pList
    set aPath to POSIX file i
    
set aPath to aPath as alias
    
set the end of aList to aPath
  end repeat
  
return aList
end getFileListWithSpotLight

★Click Here to Open This Script 

2014/12/01 2D Listを文字列長でソート

ASObjCExtras.framework v1.22で追加された、2D Listの文字列長によるソートを行うAppleScriptです。

AppleScript名:sortByLength_asoc
use AppleScript version “2.4″
use framework “Foundation”
use framework “ASObjCExtras” –ver 1.22 or later
use scripting additions

set aList to {{“Apple”, 1}, {“Orange”, 2}, {“Banana”, 100}, {“Meron”, 2}, {“Strawberry”, 0.1}, {“Lemon”, 3}, {“Takaaki Naganoya”, 5}, {“Piyomaru Software”, 11}}

set sortIndexes to {0} –Key Item id: begin from 0
set sortOrders to {true} –Ascending
set sortTypes to {“compare:”}
set sortKeys to {“length”, “self”}

set resArray to (current application’s SMSFord’s subarraysIn:(aList) sortedByIndexes:sortIndexes ascending:sortOrders sortTypes:sortTypes sortKeys:sortKeys |error|:(missing value))
set resList to resArray’s ASify() as list

–> {{”Apple”, 1}, {”Meron”, 2}, {”Lemon”, 3}, {”Orange”, 2}, {”Banana”, 100}, {”Strawberry”, 0.1}, {”Takaaki Naganoya”, 5}, {”Piyomaru Software”, 11}}

set sortIndexes to {0} –Key Item id: begin from 0
set sortOrders to {false} –Descending
set sortTypes to {“compare:”}
set sortKeys to {“length”, “self”}

set resArray to (current application’s SMSFord’s subarraysIn:(aList) sortedByIndexes:sortIndexes ascending:sortOrders sortTypes:sortTypes sortKeys:sortKeys |error|:(missing value))
set resList to resArray’s ASify() as list

–> {{”Piyomaru Software”, 11}, {”Takaaki Naganoya”, 5}, {”Strawberry”, 0.1}, {”Orange”, 2}, {”Banana”, 100}, {”Apple”, 1}, {”Meron”, 2}, {”Lemon”, 3}}

★Click Here to Open This Script 

2014/11/26 1D Listを文字列長でソート v2

1D Listの文字列長によるソートの改良版です。文字列長に着目してソートする場合には、同一長の文字列がソートされている必要はとくにないのですが、同一文字列長の場合にソートを行っています。

1D Listの文字列長ソートを掲載していたら、Shane Stanleyからツッコミが。

「それ、間違いじゃないけど文字列長でソートして、同時に(文字の)ソート順を指定しておいた方がいいよ(意訳)」

たしかに。文字列長でソートする用途しか考えていなかったので、文字列順でソートされていることは重視していませんでしたが、文字列順にソートされていた方がいい感じです。

文字列長でソートする用途というのは・・・Mail.appでSubjectに含まれるキーワードでサブフォルダに仕分けを行うさいに、文字列の長いものを優先して(長い→短い の順に)フォルダとの照合を行うロジックをAppleScriptで作って(&使って)いるためです。

このロジックを使用しているため、「Mountain Lion」のほうが「Lion」よりも優先して処理されることになり、(ほぼ)思い通りの処理をおこなえています。

US AppleがホスティングしているMailing Listにほぼすべて入っており(いくつか、US現地の教育関係者と政府関係者しか入れないMLからは丁重に追い出されましたが)、MLのログをこのようにキーワードごとに細分化したサブフォルダ(複数キーワードをカンマで区切って、類義語を列挙する仕様)に再振り分けしています。

mails.png

AppleScript名:1D Listを文字列長でソート v2
– Created 2014-11-25 by Shane Stanley
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “ASObjCExtras”

set aList to {“Apple”, “Orange”, “Banana”, “Meron”, “Strawberry”, “Lemon”, “Takaaki Naganoya”, “Piyomaru Software”}
set bList to sort1DListByStringLength(aList as list, true) of me –昇順
–> {”Apple”, “Meron”, “Lemon”, “Orange”, “Banana”, “Strawberry”, “Takaaki Naganoya”, “Piyomaru Software”}–v1
–> {”Apple”, “Lemon”, “Meron”, “Banana”, “Orange”, “Strawberry”, “Takaaki Naganoya”, “Piyomaru Software”}–v2

set cList to sort1DListByStringLength(aList as list, false) of me –降順
–> {”Piyomaru Software”, “Takaaki Naganoya”, “Strawberry”, “Orange”, “Banana”, “Apple”, “Meron”, “Lemon”}–v1
–> {”Piyomaru Software”, “Takaaki Naganoya”, “Strawberry”, “Banana”, “Orange”, “Apple”, “Lemon”, “Meron”}–v2

–1D Listを文字列長でソート v2
on sort1DListByStringLength(aList as list, sortOrder as boolean)
  set aArray to current application’s NSArray’s arrayWithArray:aList
  
set desc1 to current application’s NSSortDescriptor’s sortDescriptorWithKey:“length” ascending:sortOrder
  
set desc2 to current application’s NSSortDescriptor’s sortDescriptorWithKey:“self” ascending:true selector:“localizedCaseInsensitiveCompare:”
  
set bArray to aArray’s sortedArrayUsingDescriptors:{desc1, desc2}
  
set bList to (bArray’s ASify()) as list
  
return bList
end sort1DListByStringLength

★Click Here to Open This Script 

2014/11/25 1D Listをユニーク化してソート

1D Listをユニーク化してソートするAppleScriptです。

以前に、いろいろ1D Listのユニーク化の方法について検討していましたが、処理後のソート状態が「不定」であるため、決め手に欠けるみたいな話になっていました。

ただ、ソート状態が「不定」なら、明示的にソートすればいいだけの話で、そのように処理を足してやりました。

AppleScript名:1D Listをユニーク化してソート
– Created 2014-11-25 by Takaaki Naganoya
– 2014 Piyomaru Software
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "ASObjCExtras"

set aList to {100, 10, 1, 90, 300, 300}

set aRes to uniquifyAndSort1DList(aList, true)
–> {1, 10, 90, 100, 300}

–1D Listをユニーク化してソート
on uniquifyAndSort1DList(theList as list, aBool as boolean)
  
  
set aArray to current application’s NSArray’s arrayWithArray:theList
  
set bArray to aArray’s valueForKeyPath:"@distinctUnionOfObjects.self"
  
set aDdesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:"self" ascending:aBool selector:"compare:"
  
set cArray to bArray’s sortedArrayUsingDescriptors:{aDdesc}
  
  
set bList to (cArray’s ASify()) as list
  
  
return bList
end uniquifyAndSort1DList

★Click Here to Open This Script 

2014/11/25 1D Listを文字列長でソート

文字列だけで構成された1D Listを、各要素の文字列の長さに注目してソートするAppleScriptです。

以前に作成したルーチンのASOC版です。ただ・・・一般的には1D Listだけで処理するなんてことはほとんどなく、いくつかのデータを一緒にした2D Listで処理することになります。

2D Listの文字列長ソートも書いて(掲載しかけ)たのですが、ASObjCExtras.frameworkが2D Listのソート時に「length」でのソートをサポートしていないことがFrameworkのヘッダーファイルに書いてあったので、おクラ入りしました。残念!

AppleScript名:1D Listを文字列長でソート
– Created 2014-11-25 by Takaaki Naganoya
– 2014 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “ASObjCExtras”

set aList to {“Apple”, “Orange”, “Banana”, “Meron”, “Strawberry”, “Lemon”, “Takaaki Naganoya”, “Piyomaru Software”}
set bList to sort1DListByStringLength(aList as list, true) of me –昇順
–> {”Apple”, “Meron”, “Lemon”, “Orange”, “Banana”, “Strawberry”, “Takaaki Naganoya”, “Piyomaru Software”}

set cList to sort1DListByStringLength(aList as list, false) of me –降順
–> {”Piyomaru Software”, “Takaaki Naganoya”, “Strawberry”, “Orange”, “Banana”, “Apple”, “Meron”, “Lemon”}

–1D Listを文字列長でソート
on sort1DListByStringLength(aList as list, sortOrder as boolean)
  set aArray to current application’s NSArray’s arrayWithArray:aList
  
set descs to current application’s NSArray’s arrayWithObject:(current application’s NSSortDescriptor’s sortDescriptorWithKey:“length” ascending:sortOrder)
  
  
set bArray to aArray’s sortedArrayUsingDescriptors:descs
  
set bList to (bArray’s ASify()) as list
  
return bList
end sort1DListByStringLength

★Click Here to Open This Script 

2014/11/21 レコードのリストをソート(asoc)

レコードのリストをソートするAppleScriptです。

{{aName:”piyoko”, aVal:100}, {aName:”piyomaru”, aVal:80}, {aName:”piyoo”, aVal:10}}

のようなレコードをリスト化したデータに対して、レコードの属性ラベルを指定してその値でソートを行います。

AppleScript名:asoc_レコードのリストをソート
– Created 2014-11-21 by Takaaki Naganoya
– 2014 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “ASObjCExtras”

set aRecList to {{aName:“piyoko”, aVal:100}, {aName:“piyomaru”, aVal:80}, {aName:“piyoo”, aVal:10}}

set bList to sortRecListByLabel(aRecList, “aVal”, true) of me –昇順ソート
–> {{aName:”piyoo”, aVal:10}, {aName:”piyomaru”, aVal:80}, {aName:”piyoko”, aVal:100}}

set bList to sortRecListByLabel(aRecList, “aVal”, false) of me –降順ソート
–> {{aName:”piyoko”, aVal:100}, {aName:”piyomaru”, aVal:80}, {aName:”piyoo”, aVal:10}}

–リストに入れたレコードを、指定の属性ラベルの値でソート
on sortRecListByLabel(aRecList, aLabelStr, ascendF)
  set aArray to current application’s NSArray’s arrayWithArray:aRecList
  
  
set sortDesc to current application’s NSSortDescriptor’s alloc()’s initWithKey:aLabelStr ascending:ascendF
  
set sortDescArray to current application’s NSArray’s arrayWithObjects:sortDesc
  
set sortedArray to aArray’s sortedArrayUsingDescriptors:sortDescArray
  
  
set bList to (sortedArray’s ASify()) as list
  
return bList
end sortRecListByLabel

★Click Here to Open This Script 

2014/11/08 ASObjCExtras.frameworkのつかいかた〜2D Listのソート

ASObjCExtras.frameworkで2DのListをソートする機能を利用してみました。

AppleScriptの世界でよく利用される、ネストしたリスト、2次元配列とか2D Listとかいろいろ呼び方はありますが・・・これをソートするのにCocoaの機能を呼び出したい。

・・・のですが、なかなか同じようなものが見つからない。むしろ、Recordのソートなんかは手軽に行えるんですが、2D Listのソートがなかなか。

というニーズを受けて(?)、ASObjCExtras.frameworkの中に当たり前のように2D Listのソート機能が用意されています。

AppleScriptだけで組んだ場合と比較して(10万レコード時で)、6倍ぐらい高速に処理できます(MacBook Pro Retina Mid 2012にて計測)。

念のために、ベンチマークに用いたASOCのプログラムとAppleScriptのプログラムの両方を掲載しておきます。

graph1.png

10万レコードの2D Listの場合、AppleScriptだけでは40秒近くかかるところが7秒。そこまで大量のデータを処理するケースは少ないですが、高速なソートルーチンが使えるのであれば、使えたほうがいいに決まっています。

AppleScript名:ASObjCExtrasのじっけん_2Dリストのソート (asoc)
use AppleScript version “2.4″
use framework “Foundation”
use framework “ASObjCExtras”
use scripting additions

script orig
  property aList : {}
end script

set aList of orig to {}

set aList to {}
repeat 10000 times
  set the end of aList of orig to {“a”, random number from 1 to 10, random number from 1 to 10, random number from 1 to 100}
end repeat

set a1Dat to current application’s NSDate’s timeIntervalSinceReferenceDate()

–2D Listのソート
set sortIndexes to {1, 2, 3} –Key Item id: begin from 0
set sortOrders to {true, true, true}
set sortTypes to {“compare:”, “compare:”, “compare:”}
set resList to (current application’s SMSFord’s subarraysIn:(aList of orig) sortedByIndexes:sortIndexes ascending:sortOrders sortTypes:sortTypes |error|:(missing value)) as list

set b1Dat to current application’s NSDate’s timeIntervalSinceReferenceDate()
set c1Dat to b1Dat - a1Dat

return c1Dat

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

AppleScript名:2dlist_sort_as
use AppleScript version “2.4″
use framework “Foundation”
use scripting additions

script orig
  property aList : {}
end script

set aList of orig to {}

–複数キーによるソートテスト3(複数キーを許容)
set aList to {}
repeat 10000 times
  set the end of aList of orig to {“a”, random number from 1 to 10, random number from 1 to 10, random number from 1 to 100}
end repeat

set a1Dat to current application’s NSDate’s timeIntervalSinceReferenceDate()

set resList to multiKeySortDescending(aList of orig, {2, 3, 4}) of multiKeySort

set b1Dat to current application’s NSDate’s timeIntervalSinceReferenceDate()
set c1Dat to b1Dat - a1Dat

return c1Dat

–複数キーによるソート
script multiKeySort
  
  
script spd
    property aList : {} –ソート対象の2D Listが入る
    
property bList : {} –1次キーでソートした結果が入る
    
property cList : {} –1次キーでソートした結果のうち、1次キーで同じ値が連続する範囲が入る {{1,3},{10,20}}
    
    
property dList : {} –2次キーでソートする対象の一部のリストが入る(ワーク用)
    
property eList : {} –2次キーでソートした結果のリストが入る(ワーク用)
  end script
  
  
  
–複数キー(Primary, Secondary……)による降順ソート。キーは指定用のリストに入れる
  
on multiKeySortDescending(aList, keyList)
    
    
–Initialize
    
set aList of spd to {}
    
set bList of spd to {}
    
    
    
–Param Check
    
set aLen to length of keyList
    
if class of keyList is not equal to list then
      return false –キー値のリストがlistではなかった場合
    end if
    
    
–ソート対象の2D Listの要素数を取得
    
set tmpLen to length of first item of aList
    
repeat with i in keyList
      if i > tmpLen then
        return false –キー値として指定した内容が、ソート対象のリストの要素数よりも大きかった場合
      end if
    end repeat
    
    
    
set firstKeyNo to first item of keyList
    
    
–1次キーで2D Listをソート(降順)
    
set bList of spd to shellSortListDescending(aList, firstKeyNo) of me
    
    
–複数キーによるソート検証および実行ループ
    
repeat with iii from 1 to aLen - 1
      
      
set cList of spd to {}
      
set dList of spd to {}
      
set eList of spd to {}
      
      
–n次キーの値が連続する箇所を探す
      
set curData to missing value
      
      
set sucF to false –データ連続箇所検出中フラグ(false=非連続、true=連続中)
      
set biginItem to 0
      
set endItem to 0
      
      
set itemC to 0
      
      
repeat with i in bList of spd
        set thisData to item (item iii of keyList) of i –n次キー
        
        
–現在の値と前の値が等しい(連続箇所を検出した、あるいは連続箇所の中にいる)
        
if curData = thisData then
          
          
if sucF = false then
            set biginItem to itemC
            
set sucF to true
          else if sucF = true then
            –連続箇所の検索継続中、何もしない
          end if
          
        else
          –現在の値と前の値が等しくない(連続していない、あるいは連続箇所の末尾を検出した)
          
if sucF = true then
            set the end of cList of spd to {biginItem, itemC}
            
set sucF to false
          end if
          
          
set curData to thisData
          
        end if
        
        
set itemC to itemC + 1
        
      end repeat
      
      
–n次キーの連続状態の検出中のままリスト末尾に来た場合には、最終データを出力する
      
if sucF = true and curData = thisData then
        set the end of cList of spd to {biginItem, itemC}
      end if
      
      
      
–n次キーによる重複箇所がない場合には、n次キーによるソート結果をそのまま返す
      
if cList of spd = {} then
        return bList of spd
      end if
      
      
–n+1次キーによる部分ソートし直し
      
repeat with i in cList of spd
        set {tmpB, tmpE} to i
        
        
copy items tmpB thru tmpE of (bList of spd) to (dList of spd)
        
set (eList of spd) to shellSortListDescending((dList of spd), (item (iii + 1) of keyList)) of me
        
        
set tmpCounter to 1
        
repeat with ii from tmpB to tmpE
          copy item tmpCounter of (eList of spd) to item ii of (bList of spd)
          
set tmpCounter to tmpCounter + 1
        end repeat
      end repeat
      
    end repeat
    
    
return (bList of spd)
    
  end multiKeySortDescending
  
  
–シェルソートで入れ子のリストを降順ソート
  
on shellSortListDescending(aSortList, aKeyItem)
    script oBj
      property list : aSortList
    end script
    
set len to count oBj’s list’s items
    
set gap to 1
    
repeat while (gap len)
      set gap to ((gap * 3) + 1)
    end repeat
    
repeat while (gap > 0)
      set gap to (gap div 3)
      
if (gap < len) then
        repeat with i from gap to (len - 1)
          set temp to oBj’s list’s item (i + 1)
          
set j to i
          
repeat while ((j gap) and (contents of item aKeyItem of (oBj’s list’s item (j - gap + 1)) < item aKeyItem of temp))
            set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
            
set j to j - gap
          end repeat
          
set oBj’s list’s item (j + 1) to temp
        end repeat
      end if
    end repeat
    
return oBj’s list
  end shellSortListDescending
  
end script

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2014/10/30 ASからASOCのハンドラを呼び出す際には

昨日の問題の解決・・・小数を含む数値のリストをASOC(「AppleScriptObjC」あるいは「AppleScript/Objective-C」と呼ばれる)のハンドラに渡してソートさせた場合に、小数を含む値に丸め誤差が発生するという問題についてです。

これまでにも、Xcode上でASOCのプログラム(GUIベースのアプリケーション)は作ってきましたが、ASOCのハンドラ同士で呼び出しを行う際には極力文字列でデータをやりとりし、AppleScript側からASOC側を呼び出すことはしてきませんでした。

OS X 10.9までは、スクリプトエディタ上でもASOCのプログラムは書けたものの、あくまでアプレット上で完全にプログラムが分断された状態で動いていたために、見えてこなかった問題点でもあります。

つまり、この問題はOS X 10.10で1本のScript中で通常のAppleScriptとASOCが混在する状況が生まれたために見えてきた問題ともいえます(個人的に、OS X 10.9までのScript Editor上で記述するASOC(Interface BuilderでGUIが作れない)にあまり魅力を感じていなかったので、全然使っていなかったし)。

xcode.png

# Xcode上でも「小数点を含む数値リスト」を処理した場合には、同様の問題が出ました。Scripting Bridgeそのもののバグっぽいですね。Appleにバグレポートを書いて担当者を説得するにも時間がかかるので、目下解決策を検証中です。解決はしたのですが、自分ひとりだけでは済まないので、、、

調査したところ、list→NSArrayの型変換では問題は発生していないようでしたが、ASOCのハンドラからNSArray→listの型変換を行って値を返す際に問題が発生していました。

NSArray→listの型変換そのものはScripting Bridgeの処理系が行っているため、現状ではどーーにもなりません(Scripting Bridgeのバグ)が、「数値リスト」ではなく「数字のテキストのリスト」を扱うようにすれば、数値リストの型変換にともなう丸め誤差の発生を回避できます。

つまり、数値文字列のリストをASOC側に渡して、ソーティング(大小比較)自体を「数値として評価」すればOK。

というわけで、ASOC側には「数値リスト」ではなく「数字の文字列リスト」を渡し、ASOC側でそれぞれの要素を「数値として大小比較」するようにしてみました。念のため、有効桁数の多いfloatValueとして数値評価させています。

だいたい、AppleScriptObjCでプログラムを組む場合には、User Interface系をAppleScriptObjCで実装し、メインロジック部分は別途「通常のAppleScript」で組むことで、メンテナンス性を高めつつ、使いやすさを向上させる・・・といったケースが多いところです。

このため、AppleScriptObjCからAppleScriptを呼び出す場合、メニューやラジオボタンのID(整数)であったり、ファイルのパス(文字列)であったりと、今回の問題にひっかからない利用の仕方がほとんどでした。

OS X 10.10の「通常のAppleScriptの中にAppleScriptObjCを書けるようにする」という拡張により、これまでに少なかった「AppleScriptからAppleScriptObjCの機能を利用し、値を受け取る」という処理パターンが増え、問題点が露見したのでしょう。

AppleScript名:asoc_sorting_number_list_v2
use AppleScript version “2.4″
use framework “Foundation”
use scripting additions

set aList to {“-1.1″, “1″, “10″, “0.1″, “1.1″, “1.12″, “-1″, “-100″} –Original List
set aRes to sort1DNumList_ascOrder_(aList, true)
–> {”-100″, “-1.1″, “-1″, “0.1″, “1″, “1.1″, “1.12″, “10″}–sort ascending

set bRes to sort1DNumList_ascOrder_(aList, false)
–> {”10″, “1.12″, “1.1″, “1″, “0.1″, “-1″, “-1.1″, “-100″}–sort descending

–Sort 1-Dimension List(String Number List)
on sort1DNumList:theList ascOrder:aBool
  tell current application’s NSSet to set theSet to setWithArray_(theList)
  
tell current application’s NSSortDescriptor to set theDescriptor to sortDescriptorWithKey_ascending_(“floatValue”, aBool)
  
set sortedList to theSet’s sortedArrayUsingDescriptors:{theDescriptor}
  
return (sortedList) as list
end sort1DNumList:ascOrder:

(*
–Sort 1-Dimension List(Number List)
on sort1DNumList2:theList ascOrder:aBool
  set anArray to current application’s NSArray’s arrayWithArray:theList
  set sortedList to anArray’s sortedArrayUsingSelector:(”numberCompare:”)
  return (sortedList) as list
end sort1DNumList2:ascOrder:
*)

–Sort 1-Dimension List(String List)
on sort1DList:theList ascOrder:aBool
  set aDdesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:“self” ascending:aBool selector:“localizedCaseInsensitiveCompare:”
  
set theArray to current application’s NSArray’s arrayWithArray:theList
  
return (theArray’s sortedArrayUsingDescriptors:{aDdesc}) as list
end sort1DList:ascOrder:

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2014/10/29 AppleScriptからASOCのソートルーチンを利用する上での注意事項

OS X 10.10で一般のAppleScriptとASOCを普通にまじえて記述できるようになったため、いろいろと試してみました。

すると、ソートについていろいろと注意すべき点がわかってきたのでまとめてみます。

文字列のソート順序が違う

ためしにさまざまな文字を与えて並び順を調べてみたら、微妙に違うことがわかりました。

テストデータとして、

set aList to {”ん”, “ば”, “は”, “ぱ”, “あ”, “ま”, “ぁ”, “ア”, “か”, “ァ”}

のようなデータを用意し、昇順ソートを指定した際に、

AS: {”あ”, “ァ”, “ぁ”, “ア”, “か”, “は”, “ば”, “ぱ”, “ま”, “ん”}
ASOC:{”あ”, “ぁ”, “ア”, “ァ”, “か”, “は”, “ば”, “ぱ”, “ま”, “ん”}

と、文字の並び順が微妙に異なります。ASOC側でもうちょっとソートのパラメータを指定すると結果が変わったりしてきそうですが、とりあえず現状はこんな感じです。

どちらかといえば、ASだけで行うソートの方に不自然さを感じるぐらいですし、いまどき半角カタカナでもないでしょうから無視できそうですが・・・問題は次です。

小数を含む数値リストをソートすると丸め誤差が出る

小数を含む数値のリスト、

set aList to {1, 1, 10, 0.1, 1.1, -1, -100}

をデータとして与えてソートしてみたところ、返ってきた結果は、

–> {-100, -1, 0.10000000149, 1, 1.100000023842, 10}

微妙に値が変わってしまいました

回避策についてはいろいろ調べていますが、(US AppleのAppleScript Users MLに久しぶりに投稿してShane Stanleyに教えてもらいました)小数の値を含む数値リストのやりとりをするのは得策ではなさそうな雰囲気です(いまのところ)。

少なくとも、ASから数値をASOCのハンドラに渡しただけでは変わらないようです。

いろいろ試してみたところ、ハンドラ側でNSNumberに変換して処理を行い、結果を戻すさいに「as number」とか「as real」とかで型変換して返すときに丸め誤差が出てしまう模様です。

数値のパラメータであれば、ASから数値をASOCハンドラに渡して、丸め誤差を出さずに返すことはできたのですが、リストに入れてしまうとお手上げの状態。

まだ、試行錯誤を行ってみる必要があるようです。

AppleScript名:asocで1D Listをソート
use AppleScript version “2.4″
use framework “Foundation”
use scripting additions

–昇順ソート(ASOC)
set aList to {“ん”, “ば”, “は”, “ぱ”, “あ”, “ま”, “ぁ”, “ア”, “か”, “ァ”}
set aRes to sort1DList_ascOrder_(aList, true)
–> {”あ”, “ぁ”, “ア”, “ァ”, “か”, “は”, “ば”, “ぱ”, “ま”, “ん”}

–降順ソート(ASOC)
set bRes to sort1DList_ascOrder_(aList, false)
–> {”ん”, “ま”, “ぱ”, “ば”, “は”, “か”, “あ”, “ぁ”, “ア”, “ァ”}

–昇順ソート(AppleScript)
set cRes to shellSortAscending(aList)
–> {”あ”, “ァ”, “ぁ”, “ア”, “か”, “は”, “ば”, “ぱ”, “ま”, “ん”}

–昇順ソート(AppleScript)
set dRes to shellSortDescending(aList)
–> {”ん”, “ま”, “ぱ”, “ば”, “は”, “か”, “ぁ”, “ア”, “あ”, “ァ”}

set aList to {“1″, “01″, “10″, “000.1″, “1.1″, “-1″, “-100″}
set aRes to sort1DList_ascOrder_(aList, true)
–> {”-1″, “-100″, “000.1″, “01″, “1″, “1.1″, “10″}

set aList to {1, 1, 10, 0.1, 1.1, -1, -100}
set aRes to sort1DNumList_ascOrder_(aList, true)
–> {-100, -1, 0.10000000149, 1, 1.100000023842, 10}

–1D List(数値)をsort / ascOrderがtrueだと昇順ソート、falseだと降順ソート
on sort1DNumList:theList ascOrder:aBool
  tell current application’s NSSet to set theSet to setWithArray_(theList)
  
tell current application’s NSSortDescriptor to set theDescriptor to sortDescriptorWithKey_ascending_(missing value, true)
  
set sortedList to theSet’s sortedArrayUsingDescriptors:{theDescriptor}
  
return (sortedList) as list
end sort1DNumList:ascOrder:

–1D List(文字)をsort / ascOrderがtrueだと昇順ソート、falseだと降順ソート
on sort1DList:theList ascOrder:aBool
  set aDdesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:“self” ascending:aBool selector:“localizedCaseInsensitiveCompare:”
  
set theArray to current application’s NSArray’s arrayWithArray:theList
  
return (theArray’s sortedArrayUsingDescriptors:{aDdesc}) as list
end sort1DList:ascOrder:

–入れ子ではないリストの昇順ソート
on shellSortAscending(aSortList)
  script oBj
    property list : aSortList
  end script
  
set len to count oBj’s list’s items
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set temp to oBj’s list’s item (i + 1)
        
set j to i
        
repeat while ((j gap) and (oBj’s list’s item (j - gap + 1) > temp))
          set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
          
set j to j - gap
        end repeat
        
set oBj’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return oBj’s list
end shellSortAscending

–入れ子ではないリストの降順ソート
on shellSortDescending(aSortList)
  script oBj
    property list : aSortList
  end script
  
set len to count oBj’s list’s items
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set temp to oBj’s list’s item (i + 1)
        
set j to i
        
repeat while ((j gap) and (oBj’s list’s item (j - gap + 1) < temp))
          set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
          
set j to j - gap
        end repeat
        
set oBj’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return oBj’s list
end shellSortDescending

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

AppleScript名:asocで少数を含む数値を誤差のない形式でASに返す
–By Shane Stanley

use AppleScript version “2.4″
use framework “Foundation”
use scripting additions

set e to 0.1
set f to numberFloatWith_(e)
–> 0.1

on numberFloatWith:aNum
  set theNum to current application’s NSNumber’s numberWithFloat:aNum
  
return (theNum’s descriptionWithLocale:(current application’s NSLocale’s systemLocale())) as real
end numberFloatWith:

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2014/10/28 OS X 10.10のasocで1D Listをソート

OS X 10.10のAppleScript処理系は、マルチコアのCPUを活かして高速に処理を行うように変更されているように見えます。実際、スクリプトエディタ上でAppleScriptを実行すると、

process.png

OS X 10.9までよりもはるかに多くの内部スレッドを生成し、マシンパワーを絞り出すようにチューニングされていることがうかがわれます。

OS X 10.10ではCocoaの機能を直接呼び出すAppleScriptObjCが気軽に使えるようになったため、処理パワーが必要とされるリスト(配列)のソートをAppleScriptObjCと通常のAppleScriptで実行して処理時間を計測してみました。

処理対象の1-Dimension List(1次元配列)は10万アイテムで構成(すべて固定長の乱数を文字列化したもの)。

実行環境は、MacBook Pro Retina 2012(Core i7 2.66GHz x 4 core , 8GB RAM)およびMacBook Air 2011(Core i5 1.6GHz ,4GB RAM)です。

sort_res.png

結果は、ASOCの方が6〜18倍程度高速でした。これは、本機能を使わない手はありません。

ただ、調子に乗って100万アイテムのリストを作らせてみたら・・・RAMを使いつぶしたりスクリプトエディタを無反応にさせたりしたため、10万アイテム程度がこのぐらいのスペックのコンピュータ上でAppleScriptでデータ処理を行わせる場合には適正レベルといえるでしょう。

ぜひ、2D Listのソートもやらせたいですね。

AppleScript名:asocで1D Listをソート(じっけん)
use AppleScript version “2.4″
use framework “Foundation”
use scripting additions

script spd
  property aList : {}
end script

–テスト用データリストの作成(10万アイテム)
set (aList of spd) to {}
repeat with i from 1 to 100000
  set the end of (aList of spd) to (random number from 10000 to 99999) as string
end repeat

–昇順ソートの時間計測
set a1Dat to current date

set aRes to sort1DList_ascOrder_((aList of spd), true)
set item1 to contents of (items -9 thru -1 of aRes)

set b1Dat to current date
set c1Dat to b1Dat - a1Dat

–降順ソートの時間計測
set a2Dat to current date

set aRes to sort1DList_ascOrder_((aList of spd), false)
set item1 to contents of (items -9 thru -1 of aRes)

set b2Dat to current date
set c2Dat to b2Dat - a2Dat

return {c1Dat, c2Dat}
–> {1, 1}

–1D Listをsort / ascOrderがtrueだと昇順ソート、falseだと降順ソート
on sort1DList:theList ascOrder:aBool
  set aDdesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:“self” ascending:aBool selector:“localizedCaseInsensitiveCompare:”
  
set theArray to current application’s NSArray’s arrayWithArray:theList
  
return (theArray’s sortedArrayUsingDescriptors:{aDdesc}) as list
end sort1DList:ascOrder:

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

AppleScript名:asで1D Listをソート(じっけん)
use AppleScript version “2.4″
use scripting additions

script spd
  property aList : {}
end script

–テスト用データリストの作成(10万アイテム)
set (aList of spd) to {}
repeat with i from 1 to 100000
  set the end of (aList of spd) to (random number from 10000 to 99999) as string
end repeat

–昇順ソートの時間計測
set a1Dat to current date

set aRes to shellSortAscending(aList of spd)

set b1Dat to current date
set c1Dat to b1Dat - a1Dat

–降順ソートの時間計測
set a2Dat to current date

set aRes to shellSortDescending(aList of spd)

set b2Dat to current date
set c2Dat to b2Dat - a2Dat

return {c1Dat, c2Dat}
–> {19, 8}

–入れ子ではないリストの昇順ソート
on shellSortAscending(aSortList)
  script oBj
    property list : aSortList
  end script
  
set len to count oBj’s list’s items
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set temp to oBj’s list’s item (i + 1)
        
set j to i
        
repeat while ((j gap) and (oBj’s list’s item (j - gap + 1) > temp))
          set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
          
set j to j - gap
        end repeat
        
set oBj’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return oBj’s list
end shellSortAscending

–入れ子ではないリストの降順ソート
on shellSortDescending(aSortList)
  script oBj
    property list : aSortList
  end script
  
set len to count oBj’s list’s items
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set temp to oBj’s list’s item (i + 1)
        
set j to i
        
repeat while ((j gap) and (oBj’s list’s item (j - gap + 1) < temp))
          set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
          
set j to j - gap
        end repeat
        
set oBj’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return oBj’s list
end shellSortDescending

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2014/01/20 複数キーによるソートテスト(キー数無制限)

複数キーによる2D Listのソーティング(降順)のテスト実装を行ったAppleScriptです。

プログラムを眺めていたら、思ったよりも簡単に複数(上限なし)キー対応できることが分ったので、その場でさくっと書いてしまいました。

キー数にとくに上限はありませんが、キー数を増やせば、それだけ処理に時間がかかります。

MacBook Pro Retina(2012)Core i7 2.66GHzの環境で、リスト要素数が10,000でキーが3つだと2秒程度で処理が終了します。

# 後日、AppleStoreの店頭で3.7GHz Quad CoreのMacProで実験してみたところ、1秒だったり2秒だったりと、さほどMacBook Pro Retinaと差はありませんでした(1秒だと倍ぐらい速いことになりますが)

処理内容はおおまかにいえば……

1パス目:Primary Keyを使って2D Listをソート
2パス目:ソートずみリストのNth Keyの値を順にすべてチェック。同じ値が続く箇所をピックアップして記録
3パス目:2パス目でNth Keyの同値の内容が連続する箇所が存在していた場合には、重複部分のみ取り出して、N+1th Keyで再度ソートしてソート結果を(部分的に)差し替える

(以下、2&3パスを繰り返し)

キーはPrimaryもSecondary以下もすべて1つの1次元リスト(配列)に入れて指定します。キー値リストの内容がソート対象2D Listデータのデータ個数を超えないかどうかはチェックしていますが、キー値リストの内容の重複まではチェックしていません(キー値リストの中に0を入れてもダメ)。

スクリプト名:複数キーによるソートテスト3
script orig
  property aList : {}
end script

set aList of orig to {}

–複数キーによるソートテスト3(複数キーを許容)
set aList to {}
repeat 10000 times
  set the end of aList of orig to {“a”, random number from 1 to 10, random number from 1 to 10, random number from 1 to 100}
end repeat

set sDat to current date
set resList to multiKeySortDescending(aList of orig, {2, 3, 4}) of multiKeySort
set eDat to current date

return (eDat - sDat)

–複数キーによるソート
script multiKeySort
  
  
script spd
    property aList : {} –ソート対象の2D Listが入る
    
property bList : {} –1次キーでソートした結果が入る
    
property cList : {} –1次キーでソートした結果のうち、1次キーで同じ値が連続する範囲が入る {{1,3},{10,20}}
    
    
property dList : {} –2次キーでソートする対象の一部のリストが入る(ワーク用)
    
property eList : {} –2次キーでソートした結果のリストが入る(ワーク用)
  end script
  
  
  
–複数キー(Primary, Secondary……)による降順ソート。キーは指定用のリストに入れる
  
on multiKeySortDescending(aList, keyList)
    
    
–Initialize
    
set aList of spd to {}
    
set bList of spd to {}
    
    
    
–Param Check
    
set aLen to length of keyList
    
if class of keyList is not equal to list then
      return false –キー値のリストがlistではなかった場合
    end if
    
    
–ソート対象の2D Listの要素数を取得
    
set tmpLen to length of first item of aList
    
repeat with i in keyList
      if i > tmpLen then
        return false –キー値として指定した内容が、ソート対象のリストの要素数よりも大きかった場合
      end if
    end repeat
    
    
    
set firstKeyNo to first item of keyList
    
    
–1次キーで2D Listをソート(降順)
    
set bList of spd to shellSortListDescending(aList, firstKeyNo) of me
    
    
–複数キーによるソート検証および実行ループ
    
repeat with iii from 1 to aLen - 1
      
      
set cList of spd to {}
      
set dList of spd to {}
      
set eList of spd to {}
      
      
–n次キーの値が連続する箇所を探す
      
set curData to missing value
      
      
set sucF to false –データ連続箇所検出中フラグ(false=非連続、true=連続中)
      
set biginItem to 0
      
set endItem to 0
      
      
set itemC to 0
      
      
repeat with i in bList of spd
        set thisData to item (item iii of keyList) of i –n次キー
        
        
–現在の値と前の値が等しい(連続箇所を検出した、あるいは連続箇所の中にいる)
        
if curData = thisData then
          
          
if sucF = false then
            set biginItem to itemC
            
set sucF to true
          else if sucF = true then
            –連続箇所の検索継続中、何もしない
          end if
          
        else
          –現在の値と前の値が等しくない(連続していない、あるいは連続箇所の末尾を検出した)
          
if sucF = true then
            set the end of cList of spd to {biginItem, itemC}
            
set sucF to false
          end if
          
          
set curData to thisData
          
        end if
        
        
set itemC to itemC + 1
        
      end repeat
      
      
–n次キーの連続状態の検出中のままリスト末尾に来た場合には、最終データを出力する
      
if sucF = true and curData = thisData then
        set the end of cList of spd to {biginItem, itemC}
      end if
      
      
      
–n次キーによる重複箇所がない場合には、n次キーによるソート結果をそのまま返す
      
if cList of spd = {} then
        return bList of spd
      end if
      
      
–n+1次キーによる部分ソートし直し
      
repeat with i in cList of spd
        set {tmpB, tmpE} to i
        
        
copy items tmpB thru tmpE of (bList of spd) to (dList of spd)
        
set (eList of spd) to shellSortListDescending((dList of spd), (item (iii + 1) of keyList)) of me
        
        
set tmpCounter to 1
        
repeat with ii from tmpB to tmpE
          copy item tmpCounter of (eList of spd) to item ii of (bList of spd)
          
set tmpCounter to tmpCounter + 1
        end repeat
      end repeat
      
    end repeat
    
    
return (bList of spd)
    
  end multiKeySortDescending
  
  
–シェルソートで入れ子のリストを降順ソート
  
on shellSortListDescending(aSortList, aKeyItem)
    script oBj
      property list : aSortList
    end script
    
set len to count oBj’s list’s items
    
set gap to 1
    
repeat while (gap len)
      set gap to ((gap * 3) + 1)
    end repeat
    
repeat while (gap > 0)
      set gap to (gap div 3)
      
if (gap < len) then
        repeat with i from gap to (len - 1)
          set temp to oBj’s list’s item (i + 1)
          
set j to i
          
repeat while ((j gap) and (contents of item aKeyItem of (oBj’s list’s item (j - gap + 1)) < item aKeyItem of temp))
            set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
            
set j to j - gap
          end repeat
          
set oBj’s list’s item (j + 1) to temp
        end repeat
      end if
    end repeat
    
return oBj’s list
  end shellSortListDescending
  
end script

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2014/01/20 複数キーによるソートテスト

複数キーによる2D Listのソーティング(降順)のテスト実装を行ったAppleScriptです。

2D Listのソートルーチンはいくつも存在しており、ユーザーによるスピードの向上が行われ、充実したものとなってきました。

ただし、複数キーによるソートルーチンについては、海外を探してはみたもののなかなか見つからず、「根気づよく探すよりも、自分で作った方が早い」という状況でした。

そこで、試験的に実装してみました。MacBook Pro Retina(2012)Core i7 2.66GHzの環境で、リスト要素数が1,000で約1秒以下、要素数を10,000にしても1秒程度(テストデータの作成で時間がかかっていました)。

最短で実装できることを重視したため、3パスでソートするようにしました。

1パス目:Primary Keyを使って2D Listをソート
2パス目:ソートずみリストのPrimary Keyの値を順にすべてチェック。同じ値が続く箇所をピックアップして記録
3パス目:2パス目でPrimary Keyの同値の内容が連続する箇所が存在していた場合には、重複部分のみ取り出して、Secondary Keyで再度ソートしてソート結果を(部分的に)差し替える

という処理にしてみました。

改良の方向性はいくつかあって……

(1)昇順、降順をパラメータで指定
(2)キーの個数を無限に増やせるようにする(再帰処理による?)
(3)スピードアップのために処理内容見直し
(4)Cocoaで処理(AppleScriptObjC)

など、いろいろ考えられます。

スクリプト名:複数キーによるソートテスト2
script orig
  property aList : {}
end script

set aList of orig to {}

–複数キーによるソートテスト2
set aList to {}
repeat 1000 times
  set the end of aList of orig to {"a", random number from 1 to 10, random number from 1 to 100}
end repeat

set sDat to current date
set resList to multiKeySortDescending(aList of orig, 2, 3) of multiKeySort
set eDat to current date

return (eDat - sDat)

–複数キーによるソート
script multiKeySort
  
  
script spd
    property aList : {} –ソート対象の2D Listが入る
    
property bList : {} –1次キーでソートした結果が入る
    
property cList : {} –1次キーでソートした結果のうち、1次キーで同じ値が連続する範囲が入る {{1,3},{10,20}}
    
    
property dList : {} –2次キーでソートする対象の一部のリストが入る(ワーク用)
    
property eList : {} –2次キーでソートした結果のリストが入る(ワーク用)
  end script
  
  
  
–複数キー(Primary, Secondary)による降順ソート
  
on multiKeySortDescending(aList, firstKeyNo, secondKeyNo)
    
    
–Initialize
    
set aList of spd to {}
    
set bList of spd to {}
    
set cList of spd to {}
    
set dList of spd to {}
    
set eList of spd to {}
    
    
–1次キーで2D Listをソート(降順)
    
set bList of spd to shellSortListDescending(aList, firstKeyNo) of me
    
    
    
–1次キーの値が連続する箇所を探す
    
set curData to missing value
    
    
set sucF to false –データ連続箇所検出中フラグ(false=非連続、true=連続中)
    
set biginItem to 0
    
set endItem to 0
    
    
set itemC to 0
    
    
repeat with i in bList of spd
      set thisData to item firstKeyNo of i
      
–set thisData to contents of item firstKeyNo of i
      
      
–現在の値と前の値が等しい(連続箇所を検出した、あるいは連続箇所の中にいる)
      
if curData = thisData then
        
        
if sucF = false then
          set biginItem to itemC
          
set sucF to true
        else if sucF = true then
          –連続箇所の検索継続中、何もしない
        end if
        
      else
        –現在の値と前の値が等しくない(連続していない、あるいは連続箇所の末尾を検出した)
        
if sucF = true then
          set the end of cList of spd to {biginItem, itemC}
          
set sucF to false
        end if
        
        
set curData to thisData
        
      end if
      
      
set itemC to itemC + 1
      
    end repeat
    
    
–1次キーの連続状態の検出中のままリスト末尾に来た場合には、最終データを出力する
    
if sucF = true and curData = thisData then
      set the end of cList of spd to {biginItem, itemC}
    end if
    
    
    
–1次キーによる重複箇所がない場合には、1次キーによるソート結果をそのまま返す
    
if cList of spd = {} then
      return bList of spd
    end if
    
    
–2次キーによる部分ソートし直し
    
repeat with i in cList of spd
      set {tmpB, tmpE} to i
      
      
copy items tmpB thru tmpE of (bList of spd) to (dList of spd)
      
set (eList of spd) to shellSortListDescending((dList of spd), secondKeyNo) of me
      
      
set tmpCounter to 1
      
repeat with ii from tmpB to tmpE
        copy item tmpCounter of (eList of spd) to item ii of (bList of spd)
        
set tmpCounter to tmpCounter + 1
      end repeat
    end repeat
    
    
    
return (bList of spd)
    
  end multiKeySortDescending
  
  
–シェルソートで入れ子のリストを降順ソート
  
on shellSortListDescending(aSortList, aKeyItem)
    script oBj
      property list : aSortList
    end script
    
set len to count oBj’s list’s items
    
set gap to 1
    
repeat while (gap len)
      set gap to ((gap * 3) + 1)
    end repeat
    
repeat while (gap > 0)
      set gap to (gap div 3)
      
if (gap < len) then
        repeat with i from gap to (len - 1)
          set temp to oBj’s list’s item (i + 1)
          
set j to i
          
repeat while ((j gap) and (contents of item aKeyItem of (oBj’s list’s item (j - gap + 1)) < item aKeyItem of temp))
            set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
            
set j to j - gap
          end repeat
          
set oBj’s list’s item (j + 1) to temp
        end repeat
      end if
    end repeat
    
return oBj’s list
  end shellSortListDescending
  
end script

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/05/10 数値の2D Listから最大値を求める v4

入れ子のリスト(2次元配列)から最大値を求めるAppleScriptです。

これまで利用していた「入れ子リストのフラット化(FlattenList)」のサブルーチンが、高速なのはいいものの、リスト中の要素がすべて文字列になってしまうので、再帰で処理するルーチンをみつけてきて使ってみました。

また、不要な(マヌケな)処理がややあったので、そこのあたりはバッサリ切っています。高速化のための努力はしていないので、要素数が数千(4000以上?)に上るような処理の場合には高速化対策を考えたほうがよいでしょう。

2DのList(2次元配列)と書いていましたが、動作原理上とくに2Dである必要はなく、3Dでも4Dでも大丈夫です。

スクリプト名:数値の2D Listから最大値を求める v4
–入れ子の数値リストの最大値を求める
set rlist to {{0, 0, 0, 0, 0, 0}, {0, 1, 1, 0, 0, 0}, {1, 0, 0, 0, 1, 0}, {1, 0, 0, 1, 1, 0}, {1, 1, 1, 1, 5, 0}, {0, 0, 1, 1, 0, 0}, {0, 0, 0, 0, 0, 0}}

set aRes to getMaxFrom2DList(rlist) of me
–> 5

–2D Listから最大値を求める
on getMaxFrom2DList(rlist)
  set r2List to FlattenList(rlist) of me –2Dリストを1Dに変換
  
set maxItem to contents of first item of shellSortDescending(r2List)
  
return maxItem
end getMaxFrom2DList

–再帰タイプのリストのフラット化
–http://rosettacode.org/wiki/Flatten_a_list#AppleScript
on FlattenList(aList)
  if class of aList is not list then
    return {aList}
  else if length of aList is 0 then
    return aList
  else
    return FlattenList(first item of aList) & (FlattenList(rest of aList))
  end if
end FlattenList

–入れ子ではないリストの降順ソート
on shellSortDescending(aSortList)
  script oBj
    property list : aSortList
  end script
  
set len to count oBj’s list’s items
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set temp to oBj’s list’s item (i + 1)
        
set j to i
        
repeat while ((j gap) and (oBj’s list’s item (j - gap + 1) < temp))
          set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
          
set j to j - gap
        end repeat
        
set oBj’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return oBj’s list
end shellSortDescending

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/04/25 並び順をインデックスリストで指定してソート

並び順をインデックスリストで指定して2Dのリストを任意の並び順でソートするAppleScriptです。

日本の47都道府県に関するデータを集め……たとえば、都道府県の人口であれば、人数の多い順からソートするとか、都道府県に付けられたJISコード(全国地方公共団体コード)でソートするなど、いくつか指標となるものはあります。

ただ……都道府県を北から南へ順に並べるような処理をしたい場合には、もうその「並び順自体をデータ化」して、そのとおりにソート対象データを並べ替えるほかありません。

かくして、そのようなサブルーチンを作ってみました。

AppleScript名:並び順をインデックスリストで指定してソート
–並び順を指定するインデックスリスト(この順番にソート対象データを並べ替える)
set indexList to {"北海道", "青森県", "岩手県", "秋田県", "宮城県", "山形県", "福島県", "新潟県", "富山県", "石川県", "福井県", "茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県", "長野県", "山梨県", "岐阜県", "静岡県", "愛知県", "三重県", "滋賀県", "京都府", "兵庫県", "大阪府", "奈良県", "和歌山県", "島根県", "岡山県", "鳥取県", "広島県", "山口県", "香川県", "徳島県", "愛媛県", "高知県", "福岡県", "大分県", "佐賀県", "長崎県", "宮崎県", "熊本県", "鹿児島県", "沖縄県"}

–ソート対象データ
set aList to {{"東京都", 301}, {"神奈川県", 98}, {"大阪府", 90}, {"千葉県", 60}, {"埼玉県", 55}, {"北海道", 40}, {"愛知県", 40}, {"京都府", 40}, {"兵庫県", 40}, {"福岡県", 30}, {"静岡県", 20}, {"広島県", 20}, {"茨城県", 20}, {"宮城県", 16}, {"長野県", 15}, {"福島県", 12}, {"新潟県", 11}, {"滋賀県", 10}, {"岐阜県", 10}, {"群馬県", 10}, {"石川県", 10}, {"熊本県", 9}, {"岡山県", 9}, {"福井県", 7}, {"青森県", 7}, {"秋田県", 6}, {"愛媛県", 6}, {"和歌山県", 6}, {"奈良県", 5}, {"三重県", 5}, {"沖縄県", 5}, {"岩手県", 5}, {"大分県", 4}, {"鹿児島県", 4}, {"宮崎県", 4}, {"栃木県", 3}, {"香川県", 3}, {"山形県", 3}, {"富山県", 2}, {"山梨県", 2}, {"長崎県", 2}, {"徳島県", 2}, {"島根県", 2}, {"鳥取県", 1}, {"高知県", 1}, {"山口県", 1}}

set bList to sortItemByIndexList(aList, indexList, 1, 2) of me
–> {{"北海道", 40}, {"青森県", 7}, {"岩手県", 5}, {"秋田県", 6}, {"宮城県", 16}, {"山形県", 3}, {"福島県", 12}, {"新潟県", 11}, {"富山県", 2}, {"石川県", 10}, {"福井県", 7}, {"茨城県", 20}, {"栃木県", 3}, {"群馬県", 10}, {"埼玉県", 55}, {"千葉県", 60}, {"東京都", 301}, {"神奈川県", 98}, {"長野県", 15}, {"山梨県", 2}, {"岐阜県", 10}, {"静岡県", 20}, {"愛知県", 40}, {"三重県", 5}, {"滋賀県", 10}, {"京都府", 40}, {"兵庫県", 40}, {"大阪府", 90}, {"奈良県", 5}, {"和歌山県", 6}, {"島根県", 2}, {"岡山県", 9}, {"鳥取県", 1}, {"広島県", 20}, {"山口県", 1}, {"香川県", 3}, {"徳島県", 2}, {"愛媛県", 6}, {"高知県", 1}, {"福岡県", 30}, {"大分県", 4}, {"佐賀県", 0}, {"長崎県", 2}, {"宮崎県", 4}, {"熊本県", 9}, {"鹿児島県", 4}, {"沖縄県", 5}}

–並び順をインデックスリストで指定してソート
–パラメータ: (1) aList=ソート元リスト ({{"name1", number1}, {"name2", number2}……} )、
–      (2) indexList=ソート順指定用インデックスリスト、
–      (3) itemNo=並べ替え対象アイテム番号(ソート元リスト内の各リスト要素内のソートキーになるアイテムの番号)
–      (4) nohitItem=インデックス項目がソート元リストに対してヒットしなかった場合の、件数0を入れるアイテム番号
–エラー:    エラー時にはfalseを返す
on sortItemByIndexList(aList, indexList, itemNo, nohitItem)
  –処理高速化のためにハンドラ内部にScript Objectを宣言してデータアクセス
  
script sortItemByIndexListObj
    property indexListSpd : indexList
    
property aListSpd : aList
  end script
  
  
–パラメータのエラーチェック
  
set tmpItem to contents of first item of aList
  
set tmpLen to length of tmpItem
  
  
–itemNoのエラーチェック
  
if (itemNo < 1) or (itemNo > tmpLen) then return false
  
–nohitItemのエラーチェック
  
if (nohitItem < 1) or (nohitItem > tmpLen) then return false
  
–itemNoとnohitItemが衝突していないかチェック
  
if itemNo = nohitItem then return false
  
  
  
–処理本体
  
set outList to {}
  
repeat with i in indexListSpd of sortItemByIndexListObj
    set j to contents of i
    
    
set hitF to false
    
    
repeat with ii in aListSpd of sortItemByIndexListObj
      set targItem to contents of item itemNo of ii
      
if targItem = j then
        set the end of outList to contents of ii
        
set hitF to true
        
exit repeat
      end if
    end repeat
    
    
–合致するデータがなかった場合の対応
    
if hitF = false then
      set tmpList to {}
      
repeat nohitItem times
        set the end of tmpList to ""
      end repeat
      
set item itemNo of tmpList to j
      
set item nohitItem of tmpList to 0
      
set the end of outList to tmpList
    end if
    
  end repeat
  
  
outList
  
end sortItemByIndexList

★Click Here to Open This Script 

2013/03/14 入れ子のリストの昇順、降順ソート(超高速版)

入れ子のリスト(2次元配列)を超高速でソート(shell sort)するAppleScriptです。

以前に投稿していただいたScriptを元に、2次元配列にも対応できるよう書き換えたものです。

Core i7 2.66GHzのMacBook Pro Retina mid 2012(OS X 10.8.2)上で、1万レコードの昇順/降順ソートに1秒程度。1.6GHzのCore i5搭載のMacBook Air mid 2011で2秒程度。PowerMac G5 2.7GHz Dual Core(Mac OS X 10.4.11)上で3秒程度です。

上記MacBook Pro上で3万レコードの入れ子のリスト(2次元配列)ソートに5秒程度に対して、入れ子でないリスト(1次元配列)のソート結果が4秒程度。次元が1次元増えてこの程度の変化であれば、なかなかいい結果が出ていると思います。

スクリプト名:入れ子のリストの昇順、降順ソート
–check code 3

script spd
  property aList : {}
  
property bList : {}
end script

–Initialize
set aList of spd to {}
set bList of spd to {}

repeat 10000 times
  set the end of aList of spd to {“aaa”, random number from 1000 to 9999}
end repeat

set sTime to current date

set bList of spd to shellSortListDescending(aList of spd, 2) of me

set eTime to current date

display dialog (eTime - sTime) as string

(*
–check code 1
set aList to {{”abc”, 9}, {”bcd”, 7}, {”ccc”, 3}, {”avc”, 4}, {”dcc”, 5}}
set bList to shellSortListDescending(aList, 2) of me
–> {{”abc”, 9}, {”bcd”, 7}, {”dcc”, 5}, {”avc”, 4}, {”ccc”, 3}}

–check code 2
set cList to shellSortListAscending(aList, 2) of me
–> {{”ccc”, 3}, {”avc”, 4}, {”dcc”, 5}, {”bcd”, 7}, {”abc”, 9}}
*)

–シェルソートで入れ子のリストを昇順ソート
on shellSortListAscending(aSortList, aKeyItem)
  script oBj
    property list : aSortList
  end script
  
set len to count oBj’s list’s items
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set temp to oBj’s list’s item (i + 1)
        
set j to i
        
repeat while ((j gap) and (contents of item aKeyItem of (oBj’s list’s item (j - gap + 1)) > item aKeyItem of temp))
          set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
          
set j to j - gap
        end repeat
        
set oBj’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return oBj’s list
end shellSortListAscending

–シェルソートで入れ子のリストを降順ソート
on shellSortListDescending(aSortList, aKeyItem)
  script oBj
    property list : aSortList
  end script
  
set len to count oBj’s list’s items
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set temp to oBj’s list’s item (i + 1)
        
set j to i
        
repeat while ((j gap) and (contents of item aKeyItem of (oBj’s list’s item (j - gap + 1)) < item aKeyItem of temp))
          set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
          
set j to j - gap
        end repeat
        
set oBj’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return oBj’s list
end shellSortListDescending

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/03/13 入れ子ではないリストの昇順、降順ソート(超高速版)

入れ子ではないただのリスト(1次元配列)を、昇順・降順ソートを超高速に行うAppleScriptのサブルーチンです。

前に投稿していただいた高速ソートルーチンを、サンプルのまま整備していなかったので、降順のものを作っておきました。

あとは、日常的には入れ子のリスト(2次元配列)をソートするものが絶対に不可欠なので、このあたりを整備しておく必要があります。

ソートルーチンやリストの各種操作ルーチンを整備しまくっておいたおかげで、よほどのことがなければFileMaker Proなどのデータベースを併用する必要は感じません。

(AppleScriptを覚えて)最初のうちは、FileMaker ProやJedit上の機能におんぶにだっこのScriptを組む気楽さから、とてもこれらのアプリケーションなしで過ごすことはできなかったものですが(とくにソート)……逆にいえば、自分の作るプログラムがJeditやFileMaker Proなしの環境では動かないケースが多くなってしまうわけで、脱FileMaker Proのためにルーチンを整備しはじめたという側面もあります。

スクリプト名:入れ子ではないリストの昇順、降順ソート
set aList to {1, 2, 3, 4, 5}

set bList to shellSortDescending(aList) of me
–> {5, 4, 3, 2, 1}

set cList to shellSortAscending(aList) of me
–> {1, 2, 3, 4, 5}

–入れ子ではないリストの昇順ソート
on shellSortAscending(aSortList)
  script oBj
    property list : aSortList
  end script
  
set len to count oBj’s list’s items
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set temp to oBj’s list’s item (i + 1)
        
set j to i
        
repeat while ((j gap) and (oBj’s list’s item (j - gap + 1) > temp))
          set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
          
set j to j - gap
        end repeat
        
set oBj’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return oBj’s list
end shellSortAscending

–入れ子ではないリストの降順ソート
on shellSortDescending(aSortList)
  script oBj
    property list : aSortList
  end script
  
set len to count oBj’s list’s items
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set temp to oBj’s list’s item (i + 1)
        
set j to i
        
repeat while ((j gap) and (oBj’s list’s item (j - gap + 1) < temp))
          set oBj’s list’s item (j + 1) to oBj’s list’s item (j - gap + 1)
          
set j to j - gap
        end repeat
        
set oBj’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return oBj’s list
end shellSortDescending

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2012/03/10 シェルソート高速化比較(さらに改良)

先日投稿していただいたshell sortに、若干の改良を加えてさらに高速化したAppleScriptです。

tsuki3さんから投稿していただいたshell sortのコメントに、

ちなみに私の経験上、リストの要素数を得る場合は “lenth of ~” を使うよりも
“count ~” を用いた方が速いはずです。また、私のスクリプトでは 複数の要素を纏めて set しておりますが、
これを別個にすればより速くなるはずです。

とあったので、すべて試してみました。

sort2.jpg

MacBook Pro 2010 Core i7 2.66GHz, RAM 8GB, Mac OS X 10.6.8の環境で改良版の「shellSort5」を試してみたところ、shellSort4で5秒かかっていた3万件のソートが4秒に。正直、こんなに速くなるとは思ってもみませんでした。ループ回数が多いので、わずかなスピードアップの集積が大きいんでしょうね。

Classic Mac OSの時代には、サードパーティのOSAX(スクリプティング機能拡張)の機能を呼び出してハードウェア制御(キーボードLEDの点灯制御、シリアルポート経由の機器制御)を行ったり、高度な命令を利用することを目指していました。最終的には200個ぐらいのOSAXをシステムにインストールして使っていたほど。

ソートに関しても、サードパーティのOSAXで提供されるソート命令を利用することが多かったのですが、Mac OS Xの時代になってMac OS X対応できない(or しない)OSAXが多数あったため、OSAXへの依存度を下げることが必要になってきました。のちに、Universal Binary対応や64ビット対応など、さまざまなハードルがあってOSAX側の対応が後手後手に回る中、Mac OS Xへの移行当初からOSAXへの依存度を下げる努力をしてきたため、自分はあまり影響を受けませんでした。また、このOSAX依存度軽減の努力は、AppleScript StudioによるGUIベースのアプリケーション開発時にもおおいに役立ちました(アプリケーションと一緒に再配布できないライセンス条件のOSAXなどもあったため)。

正直なところ、ソートルーチンなんて一度作ってそこそこ満足できれば、見直すことなどほとんどない部品。最初のバージョンからここまでいろいろと高速化ができたのは、多くのScripterの方々の努力と経験によるところが大きく、1人でやっているだけでは、なかなか手が回らないところだと思います。

そういう意味では、こういうBlogを立ち上げて地道に活動してきた意義というのはあるのではないかと。この3月でスタートして4年が経過。掲載Scriptも1,000本を超えました。当初の、「サブルーチン資産に対するMac OS Xバージョンアップの影響検証」というねらいを超えて、技術的な交流の場として機能していけたらいいですね。

スクリプト名:(数値ソート) シェルソート_v2.scpt
set lst to {}
set lstr to (a reference to lst)
repeat 30000 times
  set lstr’s end to (random number 999)
end repeat
set t to (current date)

set res to my shellSort5(lst)

– 正誤確認用
(*set resr to (a reference to res)
repeat with i from 2 to (count resr)
  if (resr’s item (i - 1) > resr’s item i) then return false
end repeat*)

return {“” & ((current date) - t) & “秒”}

on shellSort1(lst) – 配列を使用した基本形
  set len to (count lst)
  
set gap to 1
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set {temp, j} to {lst’s item (i + 1), i}
        
repeat while ((j gap) and (lst’s item (j - gap + 1) > temp))
          set {lst’s item (j + 1), j} to {lst’s item (j - gap + 1), j - gap}
        end repeat
        
set lst’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return lst
end shellSort1

on shellSort2(lst) – 参照 (a reference to…) を使用
  set {lstr, len, gap} to {a reference to lst, count lst, 1}
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set {temp, j} to {lstr’s item (i + 1), i}
        
repeat while ((j gap) and (lstr’s item (j - gap + 1) > temp))
          set {lstr’s item (j + 1), j} to {lstr’s item (j - gap + 1), j - gap}
        end repeat
        
set lstr’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return lst
end shellSort2

on shellSort3(lst) – レコードと参照、グローバル変数を併用
  global rec
  
set rec to {list:lst}
  
set {recr, len, gap} to {a reference to rec, count lst, 1}
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set {temp, j} to {recr’s list’s item (i + 1), i}
        
repeat while ((j gap) and (recr’s list’s item (j - gap + 1) > temp))
          set {recr’s list’s item (j + 1), j} to {recr’s list’s item (j - gap + 1), j - gap}
        end repeat
        
set recr’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return recr’s list
end shellSort3

on shellSort4(lst) – スクリプトオブジェクトを使用
  script o
    property list : lst
  end script
  
set {len, gap} to {count o’s list’s items, 1}
  
repeat while (gap len)
    set gap to ((gap * 3) + 1)
  end repeat
  
repeat while (gap > 0)
    set gap to (gap div 3)
    
if (gap < len) then
      repeat with i from gap to (len - 1)
        set {temp, j} to {o’s list’s item (i + 1), i}
        
repeat while ((j gap) and (o’s list’s item (j - gap + 1) > temp))
          set {o’s list’s item (j + 1), j} to {o’s list’s item (j - gap + 1), j - gap}
        end repeat
        
set o’s list’s item (j + 1) to temp
      end repeat
    end if
  end repeat
  
return o’s list
end shellSort4

–プレーンなAppleScriptベースでは最速ソート
on shellSort5(aSortList) – スクリプトオブジェクトを使用
  script