OF Stats on OF3?

Seems that the modified Rob Trew OFStats script is reporting wildly inaccurate numbers. I can run the script and according to it I’ve got 189 active projects but if I actually count them there are less than 80.

This is the version I have and it’s supposed to be working with OF3 but it seems that sometime about February it started reporting inaccurate results. I haven’t really had time to deal with it but I’m finding I really miss the feedback of seeing the numbers change after my weekly review. Does anyone have any suggestions on how to fix it?

(*
Ver 0.8 adds clipboard option to dialogue
Ver 0.9 gives an error message if the cache schema has changed, leading to an SQL error in the script
Ver 1.0 slight simplification of the code
Ver 1.1 added count of Pending projects
Ver 1.2 added a count of available actions
Ver 1.3 added a break-down of unavailable actions
Ver 1.4 added count of Current projects to complement Pending projects
Ver 1.5 replaced Applescript time function with SQL time expression
Ver 1.7 reorganizes menu, and attempts to enable access for .macappstore installations of OF
Ver 1.8 adjusts handling of variant bundle identifiers generally
Ver 2.00 redraft of task breakdown, with progressive narrowing of criteria …
Ver 2.01 change for OF2. pstrDBPath reset
Ver 3.0 reworked for OF3:
- Updated query to match schema changes for tags
- Automatically checks both Omni Store and MAS containers for SQL database
- dialog presented when there is a SQL error now includes the specific SQL error message returned
- Reorganized the code, mostly separating out the query string as a property
Ver 3.1 adjusts for schema change from dropped task feature added in OF v3.4
*)

property pTitle : “OmniFocus: Quick Stats”
property pVersion : “3.1”

property pstrOmniStoreDBPath : “$HOME/Library/Containers/com.omnigroup.OmniFocus3/Data/Library/Application Support/OmniFocus/OmniFocus Caches/OmniFocusDatabase”
property pstrMacAppStoreDBPath : “$HOME/Library/Containers/com.omndigroup.OmniFocus3.MacAppStore/Data/Library/Application Support/OmniFocus/OmniFocus Caches/OmniFocusDatabase”

property pblnToClipboard : true
property pblnSubTreeCounts : true
property pToClipboard : “Copy list to clipboard”
property enableDebug : true

property pstrQuery : ¬
" SELECT “INBOX ITEMS”, COUNT() FROM task
WHERE (inInbox=1);
SELECT " Inbox action groups", COUNT(
) FROM task

								WHERE (inInbox=1) AND (childrenCount>0);
SELECT \"		Inbox actions\", COUNT(*) FROM task 
								
								WHERE (inInbox=1) AND (childrenCount=0);

SELECT null;

SELECT \"FOLDERS\", COUNT(*) FROM folder;
SELECT \"		Active folders\", COUNT(*) FROM folder 
								WHERE effectiveDateHidden is null;
SELECT \"		Dropped folders\", COUNT(*) FROM folder 
								WHERE effectiveDateHidden is not null;

SELECT null;

SELECT \"PROJECTS\", COUNT(*) FROM projectInfo 
								WHERE containsSingletonActions=0;
SELECT \"		Active projects\", COUNT(*) FROM projectInfo 
								WHERE (containsSingletonActions=0) AND (status=\"active\");
SELECT \"			Current (available) projects\", COUNT(*) FROM projectInfo p join task t on t.projectinfo=p.pk 
								WHERE (p.containsSingletonActions=0) AND (p.folderEffectiveDateHidden is null) AND (p.status=\"active\") 
								AND (t.dateToStart is null OR t.dateToStart < (strftime('%s','now') - strftime('%s','2001-01-01')));
SELECT \"			Pending (deferred) projects\", COUNT(*) FROM projectInfo p join task t on t.projectinfo=p.pk 
								WHERE (p.containsSingletonActions=0) AND (p.folderEffectiveDateHidden is null) AND (p.status=\"active\") 
								AND (t.dateToStart > (strftime('%s','now') - strftime('%s','2001-01-01')));
SELECT \"		On Hold projects\", COUNT(*) FROM projectInfo 
								WHERE (containsSingletonActions=0) AND (status=\"inactive\");
SELECT \"		Completed projects\", COUNT(*) FROM projectInfo 
								WHERE (containsSingletonActions=0) AND (status=\"done\");
SELECT \"		Dropped projects\", COUNT(*) FROM projectInfo 
								WHERE (containsSingletonActions=0) AND (( status=\"dropped\") OR (folderEffectiveDateHidden is not null));

SELECT null;

SELECT \"SINGLE ACTION LISTS\", COUNT(*) FROM projectInfo 
								WHERE containsSingletonActions=1;
SELECT \"		Active single action lists\", COUNT(*) FROM projectInfo 
								WHERE (containsSingletonActions=1) AND (status=\"active\");
SELECT \"		On Hold single action lists\", COUNT(*) FROM projectInfo 
								WHERE (containsSingletonActions=1) AND (status=\"inactive\");
SELECT \"		Completed single action lists\", COUNT(*) FROM projectInfo 
								WHERE (containsSingletonActions=1) AND (status=\"done\");
SELECT \"		Dropped single action lists\", COUNT(*) FROM projectInfo 
								WHERE (containsSingletonActions=1) AND ((status=\"dropped\") OR (folderEffectiveDateHidden is not null));

SELECT null;

SELECT \"TAGS\", COUNT(*) FROM context;
SELECT \"		Active tags\", COUNT(*) FROM context 
								WHERE (effectiveDateHidden is null) AND (allowsNextAction=1);
SELECT \"		On Hold tags\", COUNT(*) FROM context 
								WHERE (effectiveDateHidden is null) AND (allowsNextAction=0);
SELECT \"		Dropped tags\", COUNT(*) FROM context 
								WHERE effectiveDateHidden is not null;

SELECT null;

SELECT \"ACTION GROUPS\", COUNT(*) FROM task 
								WHERE (projectinfo is null) AND (childrenCount>0);
SELECT \"		Remaining action groups\", COUNT(*) FROM task 
								WHERE (projectinfo is null) AND (dateCompleted is null) AND (effectiveDateHidden is null) AND (childrenCount>0);
SELECT \"		Completed action groups\", COUNT(*) FROM task 
								WHERE (projectinfo is null) AND (dateCompleted is not null) AND (childrenCount>0);
SELECT \"		Dropped action groups\", COUNT(*) FROM task 
								WHERE (projectinfo is null) AND (effectiveDateHidden is not null) AND (childrenCount>0);

SELECT null;
	
SELECT \"ACTIONS\", COUNT(*) FROM task 
								WHERE (projectinfo is null) AND (childrenCount=0);
SELECT \"		Remaining actions\", COUNT(*) FROM task t 
								LEFT JOIN projectinfo p on t.containingProjectinfo=p.pk 
								LEFT JOIN tasktotag tg on t.persistentIdentifier=tg.task 
								LEFT JOIN context c on tg.tag=c.persistentIdentifier 
								WHERE (t.projectinfo is null) AND (t.childrenCount=0) AND (t.dateCompleted is null) 
								AND (t.containingProjectinfo is null OR (p.status !=\"dropped\" AND p.folderEffectiveDateHidden is null)) 
								AND (t.tagged=0 OR c.effectiveDateHidden is null);
SELECT \"			Available actions\", COUNT(*) FROM task t 
								LEFT JOIN projectinfo p on t.containingProjectinfo=p.pk 
								LEFT JOIN tasktotag tg on t.persistentIdentifier=tg.task 
								LEFT JOIN context c on tg.tag=c.persistentIdentifier 
								WHERE (t.projectinfo is null) AND (t.childrenCount=0) AND (t.dateCompleted is null) 
								AND (t.containingProjectinfo is null OR (p.status !=\"dropped\" AND p.folderEffectiveDateHidden is null)) 
								AND (t.tagged=0 OR c.effectiveDateHidden is null) AND (t.containingProjectInfo is null OR p.status!=\"inactive\") 
								AND (t.tagged=0 OR c.allowsNextAction=1) AND t.blocked=0;
SELECT \"			Actions in On Hold projects\", COUNT(*) FROM task t 
								LEFT JOIN projectinfo p on t.containingProjectinfo=p.pk 
								LEFT JOIN tasktotag tg on t.persistentIdentifier=tg.task 
								LEFT JOIN context c on tg.tag=c.persistentIdentifier 
								WHERE (t.projectinfo is null) AND (t.childrenCount=0) AND (t.dateCompleted is null) 
								AND (t.containingProjectinfo is null OR (p.status !=\"dropped\" AND p.folderEffectiveDateHidden is null)) 
								AND (t.tagged=0 OR c.effectiveDateHidden is null) AND (t.containingProjectInfo is not null AND p.status=\"inactive\");
SELECT \"			Actions with On Hold tags\", COUNT(*) FROM task t 
								LEFT JOIN projectinfo p on t.containingProjectinfo=p.pk 
								LEFT JOIN tasktotag tg on t.persistentIdentifier=tg.task 
								LEFT JOIN context c on tg.tag=c.persistentIdentifier 
								WHERE (t.projectinfo is null) AND (t.childrenCount=0) AND (t.dateCompleted is null) 
								AND (t.containingProjectinfo is null OR (p.status !=\"dropped\" AND p.folderEffectiveDateHidden is null)) 
								AND (t.tagged=0 OR c.effectiveDateHidden is null) AND (t.containingProjectInfo is null OR p.status=\"inactive\") 
								AND (t.tagged=1 AND c.allowsNextAction=0);
SELECT \"			Blocked actions\", COUNT(*) FROM task t 
								LEFT JOIN projectinfo p on t.containingProjectinfo=p.pk 
								LEFT JOIN tasktotag tg on t.persistentIdentifier=tg.task 
								LEFT JOIN context c on tg.tag=c.persistentIdentifier 
								WHERE (t.projectinfo is null) AND (t.childrenCount=0) AND (t.dateCompleted is null) 
								AND (t.containingProjectinfo is null OR (p.status !=\"dropped\" AND p.folderEffectiveDateHidden is null)) 
								AND (t.tagged=0 OR c.effectiveDateHidden is null) AND (t.containingProjectInfo is  null OR p.status!=\"inactive\") 
								AND (t.tagged=0 OR c.allowsNextAction=1) AND t.blocked=1;
SELECT \"				Blocked by future defer date\", COUNT(*) FROM task t 
								LEFT JOIN projectinfo p on t.containingProjectinfo=p.pk 
								LEFT JOIN tasktotag tg on t.persistentIdentifier=tg.task 
								LEFT JOIN context c on tg.tag=c.persistentIdentifier 
								WHERE (t.projectinfo is null) AND (t.childrenCount=0) AND (t.dateCompleted is null) 
								AND (t.containingProjectinfo is null OR (p.status !=\"dropped\" AND p.folderEffectiveDateHidden is null)) 
								AND (t.tagged=0 OR c.effectiveDateHidden is null) AND (t.containingProjectInfo is null AND p.status!=\"inactive\") 
								AND (t.tagged=0 OR c.allowsNextAction=1) AND t.blocked=1 AND t.blockedByFutureStartDate=1;
SELECT \"				Sequentially blocked\", COUNT(*) FROM task t 
								LEFT JOIN projectinfo p on t.containingProjectinfo=p.pk 
								LEFT JOIN tasktotag tg on t.persistentIdentifier=tg.task 
								LEFT JOIN context c on tg.tag=c.persistentIdentifier 
								WHERE (t.projectinfo is null) AND (t.childrenCount=0) AND (t.dateCompleted is null) 
								AND (t.containingProjectinfo is null OR (p.status !=\"dropped\" AND p.folderEffectiveDateHidden is null)) 
								AND (t.tagged=0 OR c.effectiveDateHidden is null) AND (t.containingProjectInfo is null OR p.status!=\"inactive\") 
								AND (t.tagged=0 OR c.allowsNextAction=1) AND t.blocked=1 AND t.blockedByFutureStartDate=0;
SELECT \"		Completed actions\", COUNT(dateCompleted) FROM task 
								WHERE (projectinfo is null) AND (childrenCount=0);		
SELECT \"		Dropped actions\", COUNT (*) FROM task t  
								LEFT JOIN projectinfo p on t.containingProjectinfo=p.pk  
								LEFT JOIN tasktotag tg on t.persistentIdentifier=tg.task  
								LEFT JOIN context c on tg.tag=c.persistentIdentifier 
								WHERE (t.projectinfo is null AND t.childrenCount=0  AND t.dateCompleted is null) 
								AND ((t.effectiveDateHidden is not null) OR ((t.containingProjectinfo is not null) 
								AND (p.status=\"dropped\" OR p.folderEffectiveDateHidden is not null)) OR ((t.containingProjectinfo is null) 
								OR ((p.status !=\"dropped\") AND (p.folderEffectiveDateHidden is null)) AND (c.effectiveDateHidden is not null)));
SELECT \"			Dropped directly\", COUNT(*) FROM task 
								WHERE (projectinfo is null) AND (childrenCount=0)  AND (dateCompleted is null) AND (effectiveDateHidden is not null);
SELECT \"			Dropped by project\", COUNT(*) FROM task t 
								LEFT JOIN projectinfo p on t.containingProjectinfo=p.pk 
								WHERE (t.projectinfo is null) AND (childrenCount=0)  AND (t.dateCompleted is null) 
								AND (t.containingProjectinfo is not null AND (p.status=\"dropped\" OR p.folderEffectiveDateHidden is not null));
SELECT \"			Dropped by tag\", COUNT(*) FROM task t 
								LEFT JOIN projectinfo p on t.containingProjectinfo=p.pk  
								LEFT JOIN tasktotag tg on t.persistentIdentifier=tg.task  
								LEFT JOIN context c on tg.tag=c.persistentIdentifier  
								WHERE (t.projectinfo is null) AND (t.childrenCount=0)  AND (t.dateCompleted is null)  
								AND (t.containingProjectinfo is null OR (p.status !=\"dropped\" AND p.folderEffectiveDateHidden is null)) 
								AND (t.tagged=1 AND c.effectiveDateHidden is not null);

"

on run

-- Check to see if there is a OmniFocus SQL database cache
set omniStorPathExists to checkPathExists(pstrOmniStoreDBPath)
set macAppStorePathExists to checkPathExists(pstrMacAppStoreDBPath)

if omniStorPathExists then
	set strDBPath to pstrOmniStoreDBPath
else if macAppStorePathExists then
	set strDBPath to pstrMacAppStoreDBPath
else
	display dialog "OmniFocus cache (SQL database) not found ..." buttons {"OK"} default button 1 ¬
		with title pTitle & " Ver. " & pVersion
	return
end if

try
	-- Run query to get list of stats
	set strStatsList to do shell script "sqlite3 -separator ': ' \"" & strDBPath & "\" " & quoted form of pstrQuery
	
on error errMsg
	display dialog ¬
		errMsg & ¬
		return & return & ¬
		"The SQL schema for the OmniFocus cache may have changed in a recent update of OF." & ¬
		return & return & ¬
		"Look on the OmniFocus user forums for an updated version of this script." buttons {"OK"} ¬
		with title pTitle & "Ver. " & pVersion
	return
end try

--Show the results
activate
set statsWindow to display dialog strStatsList buttons {pToClipboard, "OK"} default button ¬
	"OK" with title pTitle & " Ver. " & pVersion

if button returned of statsWindow is pToClipboard then set the clipboard to strStatsList

end run

on checkPathExists(strDBPath)
set pathExists to do shell script “test -f “” & strDBPath & “” && echo ‘true’ || echo ‘false’”

if pathExists is "true" then
	return true
else
	return false
end if

end checkPathExists

I could try rewriting this wonderful script with the new OmniJS API. Will look into it.

Could you check if this first draft yields correct results ?

You could run it in Script Editor, for example (setting language tab to JavaScript)

(() => {
    'use strict';

    // @unlocked2412
    // Ver 0.02

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = strName => {
        // main :: IO ()
        const main = () => {
            const xs = ['Folder', 'Project', 'Tag', 'Task']
            return intercalate('\n\n')(
                map(
                    compose(
                        statsString,
                        second(statsFromObjs),
                        flattenedObjects(this)
                    )
                )(xs)
            )
        };
        const flattenedObjects = parent => type =>
            Tuple(type)(parent['flattened' + type + 's'])

        const statsFromObjs = xs => {
            const tupleFromObj =
                fanArrow(
                    compose(
                        enumValName,
                        x => x[x.constructor.name === 'Task' ? 'taskStatus' : 'status']
                    )
                )(x => x.name)
            return compose(
                map(second(length)),
                groupSort,
                map(tupleFromObj)
            )(xs)
        }

        const statsString = tpl =>
            `${toUpper(fst(tpl))}S:
${unlines(map(uncurry(f(fst(tpl))))(snd(tpl)))}`

        const f = type => x => y =>
            `${x} ${toLower(type)}s: ${y}`


        // FUNCTIONS --
        // https://github.com/RobTrew/prelude-jxa

        // JS Prelude --------------------------------------------------
        // Just :: a -> Maybe a
        const Just = x => ({
            type: 'Maybe',
            Nothing: false,
            Just: x
        });

        // Left :: a -> Either a b
        const Left = x => ({
            type: 'Either',
            Left: x
        });

        // Nothing :: Maybe a
        const Nothing = () => ({
            type: 'Maybe',
            Nothing: true,
        });

        // Right :: b -> Either a b
        const Right = x => ({
            type: 'Either',
            Right: x
        });

        // Tuple (,) :: a -> b -> (a, b)
        const Tuple = a =>
            b => ({
                type: 'Tuple',
                '0': a,
                '1': b,
                length: 2
            });

        // append (++) :: [a] -> [a] -> [a]
        const append = xs =>
            // A list defined by the
            // concatenation of two others.
            ys => xs.concat(ys);

        // bind (>>=) :: Monad m => m a -> (a -> m b) -> m b
        const bind = m =>
            mf => (Array.isArray(m) ? (
                bindList
            ) : (() => {
                const t = m.type;
                return 'Either' === t ? (
                    bindLR
                ) : 'Maybe' === t ? (
                    bindMay
                ) : 'Tuple' === t ? (
                    bindTuple
                ) : ('function' === typeof m) ? (
                    bindFn
                ) : undefined;
            })()(m)(mf));

        // bindFn (>>=) :: (a -> b) -> (b -> a -> c) -> a -> c
        const bindFn = f =>
            // Binary operator applied over f x and x.
            bop => x => bop(f(x))(x);

        // bindLR (>>=) :: Either a -> 
        // (a -> Either b) -> Either b
        const bindLR = m =>
            mf => undefined !== m.Left ? (
                m
            ) : mf(m.Right);

        // bindList (>>=) :: [a] -> (a -> [b]) -> [b]
        const bindList = xs =>
            mf => [...xs].flatMap(mf);

        // bindMay (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
        const bindMay = mb =>
            // Nothing if mb is Nothing, or the application of the
            // (a -> Maybe b) function mf to the contents of mb.
            mf => mb.Nothing ? (
                mb
            ) : mf(mb.Just);

        // bindTuple (>>=) :: Monoid a => (a, a) -> (a -> (a, b)) -> (a, b)
        const bindTuple = tpl =>
            f => {
                const t2 = f(tpl[1]);
                return Tuple(mappend(tpl[0])(t2[0]))(
                    t2[1]
                );
            };

        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            x => y => {
                const
                    a = f(x),
                    b = f(y);
                return a < b ? -1 : (a > b ? 1 : 0);
            };

        // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
        const compose = (...fs) =>
            // A function defined by the right-to-left
            // composition of all the functions in fs.
            fs.reduce(
                (f, g) => x => f(g(x)),
                x => x
            );

        // concat :: [[a]] -> [a]
        // concat :: [String] -> String
        const concat = xs => (
            ys => 0 < ys.length ? (
                ys.every(Array.isArray) ? (
                    []
                ) : ''
            ).concat(...ys) : ys
        )(list(xs));

        // eq (==) :: Eq a => a -> a -> Bool
        const eq = a =>
            // True when a and b are equivalent in the terms
            // defined below for their shared data type.
            b => {
                const t = typeof a;
                return t !== typeof b ? (
                    false
                ) : 'object' !== t ? (
                    'function' !== t ? (
                        a === b
                    ) : a.toString() === b.toString()
                ) : (() => {
                    const kvs = Object.entries(a);
                    return kvs.length !== Object.keys(b).length ? (
                        false
                    ) : kvs.every(([k, v]) => eq(v)(b[k]));
                })();
            };

        // fanArrow (&&&) :: (a -> b) -> (a -> c) -> (a -> (b, c))
        const fanArrow = f =>
            // A function from x to a tuple of (f(x), g(x))
            // ((,) . f <*> g)
            g => x => Tuple(f(x))(
                g(x)
            );

        // find :: (a -> Bool) -> [a] -> Maybe a
        const find = p =>
            // Just the first element in xs which 
            // matches the predicate p, or
            // Nothing if no match is found.
            xs => xs.constructor.constructor.name !== (
                'GeneratorFunction'
            ) ? (() => {
                const
                    ys = list(xs),
                    i = ys.findIndex(p);
                return -1 !== i ? (
                    Just(ys[i])
                ) : Nothing();
            })() : findGen(p)(xs);

        // findGen :: (a -> Bool) -> Gen [a] -> Maybe a
        const findGen = p =>
            // Just the first match for the predicate p
            // in the generator stream xs, or Nothing
            // if no match is found.
            xs => {
                const
                    mb = until(tpl => {
                        const nxt = tpl[0];
                        return nxt.done || p(nxt.value);
                    })(
                        tpl => Tuple(tpl[1].next())(
                            tpl[1]
                        )
                    )(Tuple(xs.next())(xs))[0];
                return mb.done ? (
                    Nothing()
                ) : Just(mb.value);
            };

        // findIndex :: (a -> Bool) -> [a] -> Maybe Int
        const findIndex = p =>
            //  Just the index of the first element in
            //  xs for which p(x) is true, or
            //  Nothing if there is no such element.
            xs => {
                const i = [...xs].findIndex(p);
                return -1 !== i ? (
                    Just(i)
                ) : Nothing();
            };

        // fst :: (a, b) -> a
        const fst = tpl =>
            // First member of a pair.
            tpl[0];

        // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
        const groupBy = fEq =>
            // Typical usage: groupBy(on(eq)(f), xs)
            xs => (ys => 0 < ys.length ? (() => {
                const
                    tpl = ys.slice(1).reduce(
                        (gw, x) => {
                            const
                                gps = gw[0],
                                wkg = gw[1];
                            return fEq(wkg[0])(x) ? (
                                Tuple(gps)(wkg.concat([x]))
                            ) : Tuple(gps.concat([wkg]))([x]);
                        },
                        Tuple([])([ys[0]])
                    ),
                    v = tpl[0].concat([tpl[1]]);
                return 'string' !== typeof xs ? (
                    v
                ) : v.map(x => x.join(''));
            })() : [])(list(xs));

        // head :: [a] -> a
        const head = xs => (
            ys => ys.length ? (
                ys[0]
            ) : undefined
        )(list(xs));

        // identity :: a -> a
        const identity = x =>
            // The identity function.
            x;

        // intercalate :: [a] -> [[a]] -> [a]
        // intercalate :: String -> [String] -> String
        const intercalate = sep => xs =>
            0 < xs.length && 'string' === typeof sep &&
            'string' === typeof xs[0] ? (
                xs.join(sep)
            ) : concat(intersperse(sep)(xs));

        // intersperse :: a -> [a] -> [a]
        // intersperse :: Char -> String -> String
        const intersperse = sep => xs => {
            // intersperse(0, [1,2,3]) -> [1, 0, 2, 0, 3]
            const bln = 'string' === typeof xs;
            return xs.length > 1 ? (
                (bln ? concat : x => x)(
                    (bln ? (
                        xs.split('')
                    ) : xs)
                    .slice(1)
                    .reduce((a, x) => a.concat([sep, x]), [xs[0]])
                )) : xs;
        };

        // join :: Monad m => m (m a) -> m a
        const join = x =>
            bind(x)(identity);

        // keys :: Dict -> [String]
        const keys = Object.keys;

        // length :: [a] -> Int
        const length = xs =>
            // Returns Infinity over objects without finite
            // length. This enables zip and zipWith to choose
            // the shorter argument when one is non-finite,
            // like cycle, repeat etc
            'GeneratorFunction' !== xs.constructor
            .constructor.name ? (
                xs.length
            ) : Infinity;

        // list :: StringOrArrayLike b => b -> [a]
        const list = xs =>
            // xs itself, if it is an Array,
            // or an Array derived from xs.
            Array.isArray(xs) ? (
                xs
            ) : Array.from(xs || []);

        // map :: (a -> b) -> [a] -> [b]
        const map = f =>
            // The list obtained by applying f
            // to each element of xs.
            // (The image of xs under f).
            xs => [...xs].map(f);

        // mappend (<>) :: Monoid a => a -> a -> a
        const mappend = a =>
            // Associative operation 
            // defined for various monoid types.
            b => (t => (Boolean(t) ? (
                'Maybe' === t ? (
                    mappendMaybe
                ) : mappendTuple
            ) : Array.isArray(a) ? (
                append
            ) : 'function' === typeof a ? (
                mappendFn
            ) : mappendOrd)(a)(b))(a.type);

        // mappendFn :: Monoid b => (a -> b) -> (a -> b) -> (a -> b)
        const mappendFn = f =>
            g => x => mappend(f(x))(
                g(x)
            );

        // mappendMaybe (<>) :: Maybe a -> Maybe a -> Maybe a
        const mappendMaybe = a =>
            b => a.Nothing ? (
                b
            ) : b.Nothing ? (
                a
            ) : Just(
                mappend(a.Just)(
                    b.Just
                )
            );

        // mappendOrd (<>) :: Ordering -> Ordering -> Ordering
        const mappendOrd = x =>
            y => 0 !== x ? (
                x
            ) : y;

        // mappendTuple (<>) :: (a, b) -> (a, b) -> (a, b)
        const mappendTuple = t => t1 =>
            Tuple(
                mappend(t[0])(t1[0])
            )(
                mappend(t[1])(t1[1])
            );

        // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
        const on = f =>
            // e.g. groupBy(on(eq)(length))
            g => a => b => f(g(a))(g(b));

        // second :: (a -> b) -> ((c, a) -> (c, b))
        const second = f =>
            // A function over a simple value lifted
            // to a function over a tuple.
            // f (a, b) -> (a, f(b))
            xy => Tuple(xy[0])(
                f(xy[1])
            );

        // snd :: (a, b) -> b
        const snd = tpl =>
            // Second member of a pair.
            tpl[1];

        // sort :: Ord a => [a] -> [a]
        const sort = xs => list(xs).slice()
            .sort((a, b) => a < b ? -1 : (a > b ? 1 : 0));

        // sortOn :: Ord b => (a -> b) -> [a] -> [a]
        const sortOn = f =>
            // Equivalent to sortBy(comparing(f)), but with f(x)
            // evaluated only once for each x in xs.
            // ('Schwartzian' decorate-sort-undecorate).
            xs => xs.map(
                x => Tuple(f(x))(x)
            )
            .sort(uncurry(comparing(fst)))
            .map(snd);

        // toLower :: String -> String
        const toLower = s =>
            // Lower-case version of string.
            s.toLocaleLowerCase();

        // toUpper :: String -> String
        const toUpper = s =>
            s.toLocaleUpperCase();

        // uncurry :: (a -> b -> c) -> ((a, b) -> c)
        const uncurry = f =>
            // A function over a pair, derived
            // from a curried function.
            function () {
                const
                    args = arguments,
                    xy = Boolean(args.length % 2) ? (
                        args[0]
                    ) : args;
                return f(xy[0])(xy[1]);
            };

        // unlines :: [String] -> String
        const unlines = xs =>
            // A single string formed by the intercalation
            // of a list of strings with the newline character.
            xs.join('\n');

        // until :: (a -> Bool) -> (a -> a) -> a -> a
        const until = p =>
            f => x => {
                let v = x;
                while (!p(v)) v = f(v);
                return v;
            };

        // JS Basics ---------------------------------------------------
        // groupOn :: Eq b => (a -> b) -> [a] -> [[a]]
        const groupOn = f => groupBy(on(eq)(f))

        // groupSort :: Ord k => [(k, v)] -> [(k, [v])]
        const groupSort = compose(
            map(x => Tuple(fst(head(x)))(map(snd)(x))),
            groupOn(fst),
            sortOn(fst)
        )

        // OmniFocus OmniJS --------------------------------------------
        // enumValName :: EnumValue -> String
        const enumValName = x => {
            const dct = x.constructor;
            return Object.getOwnPropertyNames(dct)
                .find(k => x === dct[k]);
        };

        return main();
    };


    // OmniJS Context Evaluation ------------------------------------------------
    return 0 < Application('OmniFocus').documents.length ? (
        Application('OmniFocus').evaluateJavascript(
            `(${omniJSContext})()`
        )
    ) : 'No documents open in OmniFocus.'
})();

Got this error

Error: Error: SyntaxError: Unexpected identifier ‘file’ undefined:3:0

Cannot reproduce that error.

What operating system are you using ?

Catalina 10.15.7

Ok. You can certainly run this script, then.

Did you set this setting in Script Editor ?

Excuse me, @OogieM. I will update a fixed version in a moment.

Just fixed the code in that post. Could you try again, please ?

OK I tried it and it’s getting the correct number of projects and tags.
Missing all the detail about actions

Here’s what the old script would show
INBOX ITEMS: 11
Inbox action groups: 0
Inbox actions: 11

FOLDERS: 13
Active folders: 13
Dropped folders: 0

PROJECTS: 267
Active projects: 182
Current (available) projects: 159
Pending (deferred) projects: 23
On Hold projects: 27
Completed projects: 44
Dropped projects: 14

SINGLE ACTION LISTS: 2
Active single action lists: 2
On Hold single action lists: 0
Completed single action lists: 0
Dropped single action lists: 0

TAGS: 42
Active tags: 42
On Hold tags: 0
Dropped tags: 0

ACTION GROUPS: 97
Remaining action groups: 62
Completed action groups: 35
Dropped action groups: 0

ACTIONS: 1777
Remaining actions: 1300
Available actions: 300
Actions in On Hold projects: 246
Actions with On Hold tags: 0
Blocked actions: 754
Blocked by future defer date: 0
Sequentially blocked: 284
Completed actions: 494
Dropped actions: 42
Dropped directly: 34
Dropped by project: 26
Dropped by tag: 0

And here is what i got from your new script
Active folders: 17
Dropped folders: 0
Active projects: 169
OnHold projects: 26
Done projects: 1
Dropped projects: 0
Active tags: 46
OnHold tags: 0
Dropped tags: 0

I will see if I can re-create the script to provide a greater set of statistics.

Updated to Ver 0.02

What do you think @OogieM, now ?

1 Like

That provides most of the info I really care about.

Is there any way to add it as a script in the toolbar of Omnifocu lke the applescript ones can be added or do I have to always run it in the script editor?

Tomorrow (or perhaps on Thursday), I am going to do a cross-platform script. After placing it in your OmniFocus iCloud folder (or any linked folder), an entry is going to appear in Automation menu for quick execution).

I don’t use iCloud at all and do not use Omnifocus Web either.

Would it go in the Omnifocus Scripts folder then?

@OogieM, you could link any folder you want. Simply click on Add Linked Folder...

Are you using OmniFocus iOS version ?

@OogieM, just published the PlugIn. Does it work for you ?

Link:

Trying to figure out how to download and install it at the moment. I’ve never added any plugins to Omnifocus and I don’t really want to fork the repo as I don’t plan on making changes.

I use both desktop and iOS versions of Omnifocus

@OogieM, well you can download a zip file that contains the script.

In any case, here is the script:

OF Statistics.omnijs (26.7 KB)

Tell me if you have any troubles linking your folder.

1 Like