XBRL can be a challenging topic to tackle. There are a lot of rules to learn for it, and with 14,000+ elements in the US:GAAP taxonomy, it doesn’t look like it’s going to get simpler any time soon. Fortunately, using GoFiler and Legato, we can add some functionality to make things a little easier.
Legato supports XBRL through an XBRL object. You can retrieve a handle to the XBRL object by using the XBRLGetObject function. The handle is a required parameter for most functions dealing with XBRL. In the current version of Legato, we have the ability to read various parts of any XBRL open in the XBRL View. We can do things like get elements, get labels, get presentations, and so on. Future versions of Legato will have the ability to modify these attributes, but for now the XBRL object can only read them. This is still pretty useful. We can do things like add extra validations and checks to make sure we’re doing things the way we want. I wrote up a small example script of how to use these functions. This little program adds an extra check to the validate button to check to see if the number of extended elements is too high, but there are many more things you can do with a script like this.
The example script:
#define HIGH 30
#define MID 15
#define HIGH_MSG "Using a high percentage of extended elements is not recommended. Use more taxonomy elements."
#define MID_MSG "Custom element usage is about average, if possible replace some custom definitions for taxonomy elements."
void setup();
void run(int f_id, string mode, handle window);
void main(){
int ix;
int size;
string windows[][];
handle window;
if (GetScriptParent() == "LegatoIDE"){
windows = EnumerateEditWindows();
size = ArrayGetAxisDepth(windows);
for (ix = 0 ; ix < size; ix++){
if (windows[ix]["FileTypeToken"] == "FT_XFR"){
run (0,"postprocess",MakeHandle(windows[ix]["ClientHandle"]));
}
}
}
setup();
}
void setup(){
MenuSetHook("XBRL_VALIDATE", GetScriptFilename(), "run");
MenuSetHook("EDGAR_VALIDATE", GetScriptFilename(), "run");
}
void run(int f_id, string mode, handle window){
handle XBRL;
string presentations[];
string elements[];
dword wType;
int fields_pos;
int customs;
int total_elements;
int ix, rx;
int percent;
int size;
boolean hooked;
if (mode != "postprocess"){
return;
}
if (IsWindowHandleValid(window) == false){
window = GetActiveEditWindow();
wType = GetEditWindowType(window) & EDX_TYPE_ID_MASK;
if (wType != EDX_TYPE_XBRL_VIEW){
return;
}
hooked = true;
}
XBRL = XBRLGetObject(window);
presentations = XBRLGetPresentations(XBRL);
fields_pos = FindInList(presentations,"XBRL Financial Fields");
if (fields_pos < 0){
return;
}
elements = XBRLGetPresentationElements(XBRL,fields_pos);
size = ArrayGetAxisDepth(elements);
customs = 0;
for (ix = 0; ix < size; ix++){
if (elements[ix] !=""){
total_elements++;
if (FindInString(elements[ix],"custom:")==0){
customs++;
}
}
}
percent = (customs*100)/total_elements;
if (hooked == false){
AddMessage("Checking Custom Elements in %s",GetEditWindowFilename(window));
AddMessage("Found %d total line items",total_elements);
AddMessage("Found %d custom line items",customs);
AddMessage("%d%% custom line items",percent);
}
else{
if (percent > MID){
if (percent >= HIGH){
MessageBox('x',"%d%% custom elements used as line items. %s",percent,HIGH_MSG);
}
else{
MessageBox('i',"%d%% custom elements used as line items. %s",percent,MID_MSG);
}
}
}
}
This script is set up so it can run from either the Legato IDE or from XBRL view. If it’s run from the IDE, the main function is going to be run otherwise setup will run. So, the first thing that function needs to do is double-check to make sure it’s being run from the IDE by using the GetScriptParent function. Once we know that it’s running from an IDE, we can iterate over all windows, and if the window is of file type FT_XFR, we know it’s an XBRL file and we can use our run function on it. The run function in this case is set up to take a window handle as an input parameter, so all we need to do is pass the handle of our XBRL window to the function.
void main(){
int ix;
int size;
string windows[][];
handle window;
if (GetScriptParent() == "LegatoIDE"){
windows = EnumerateEditWindows();
size = ArrayGetAxisDepth(windows);
for (ix = 0 ; ix < size; ix++){
if (windows[ix]["FileTypeToken"] == "FT_XFR"){
run (0,"postprocess",MakeHandle(windows[ix]["ClientHandle"]));
}
}
}
setup();
}
The setup function is really simple. We just want to hook our run function to the XBRL validate and the regular EDGAR validate buttons. The run function itself then needs to double-check to make sure it actually has an XBRL view open before it continues.
void setup(){
MenuSetHook("XBRL_VALIDATE", GetScriptFilename(), "run");
MenuSetHook("EDGAR_VALIDATE", GetScriptFilename(), "run");
}
The run function is the main function in this script. The first thing it does is check to make sure we’re running in postprocess mode. We want this to run after the normal validation. Then we can check to see if we were passed a valid window handle. If so, we’re running in IDE mode. If not, then we can get the active edit window, ensure it’s an XBRL view, and set the “hooked” variable to true to show we’re running in hooked mode instead of IDE mode. Then we can use the XBRLGetObject function to grab a handle to the XBRL object. Using this handle, we can get all sorts of data out of our XBRL file, but in this case we’re going to have the XBRLGetPresentations function get a list of all XBRL presentations. We can then use the FindInList function to find the position of the XBRL Fields presentation, which has all of the used elements listed on it in GoFiler. If there isn’t one, we can just return here. Otherwise, we can use the XBRLGetPresentationElements function to retrieve a list of all the elements used in the entire report,and use the ArrayGetAxisDepth function to get the size of that list.
void run(int f_id, string mode, handle window){
<... declarations omitted ... >
if (mode != "postprocess"){
return;
}
if (IsWindowHandleValid(window) == false){
window = GetActiveEditWindow();
wType = GetEditWindowType(window) & EDX_TYPE_ID_MASK;
if (wType != EDX_TYPE_XBRL_VIEW){
return;
}
hooked = true;
}
XBRL = XBRLGetObject(window);
presentations = XBRLGetPresentations(XBRL);
fields_pos = FindInList(presentations,"XBRL Financial Fields");
if (fields_pos < 0){
return;
}
elements = XBRLGetPresentationElements(XBRL,fields_pos);
size = ArrayGetAxisDepth(elements);
Now that we have the list of elements and the size, we can iterate over the list. For each element, we add it to the total number of elements. Also, if the element name contains “custom:” at the start, then it must be a custom element, so we can add one to the total number of custom elements. The percentage of custom elements is going to be number of custom elements multiplied by 100 and then divided by the total number of elements. I multiply by 100 before dividing so I can use integer math to do the calculation instead of a floating point calculation. If we’re running in “hooked” mode, we can then just add some messages to the log. If not, we want to evaluate if the percentage of custom elements is too high, so we compare it to our defined middle and high threshold points and print out appropriate error messages in either case.
customs = 0;
for (ix = 0; ix < size; ix++){
if (elements[ix] !=""){
total_elements++;
if (FindInString(elements[ix],"custom:")==0){
customs++;
}
}
}
percent = (customs*100)/total_elements;
if (hooked == false){
AddMessage("Checking Custom Elements in %s",GetEditWindowFilename(window));
AddMessage("Found %d total line items",total_elements);
AddMessage("Found %d custom line items",customs);
AddMessage("%d%% custom line items",percent);
}
else{
if (percent > MID){
if (percent >= HIGH){
MessageBox('x',"%d%% custom elements used as line items. %s",percent,HIGH_MSG);
}
else{
MessageBox('i',"%d%% custom elements used as line items. %s",percent,MID_MSG);
}
}
}
}
Like most of our examples, this is only meant to be an overview of something you can do with the XBRL object. It can get fact values, context values, labels, pretty much any data you could want out of an XBRL report. We’re using it here to add an extra layer of validation, but it could just as easily be adjusted to mine existing reports for data. If you mined the SEC for XBRL files, for example, you could write a script to iterate through them and pull out key facts across them all. Any XBRL file that can be opened in GoFiler can work with the XBRL object, and therefore Legato can help you automate the retrieval of information from them. Full documentation of what the XBRL object can do is available in the Legato Documentation located here.
Steven Horowitz has been working for Novaworks for over five years as a technical expert with a focus on EDGAR HTML and XBRL. Since the creation of the Legato language in 2015, Steven has been developing scripts to improve the GoFiler user experience. He is currently working toward a Bachelor of Sciences in Software Engineering at RIT and MCC. |
Additional Resources
Novaworks’ Legato Resources
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato