This week, we will be continuing to write our XBRL Merger script. The previous post covered how to prompt the user for a pair of files, checked to make sure those files were valid XFR files that we could actually access, and finally made sure that they weren’t the same files. This week we’re taking it a step further by opening the files (or switching to them if they are already open), exporting them to new directories, and comparing the contents of these directories. This script is intended to merge two very similar files, where the only difference is a couple of facts being tagged with a different unit, so the exported file sets need to very closely match.
Friday, April 28. 2017
LDC #32: XBRL Merger, Part 2
The new script is below, but here is a quick summary of changes from last week:
1 ) New global variables and defines have been added.
2 ) The run function has been modified to take the result from our dialog.
3 ) The merge_ok function has been modified to store the value of our user input. It also now stores the difference in size of the filenames.
4 ) The export_file function has been added. It simply exports the file it’s given.
5 ) The get_export_folder function has been added. It ensures our output folder exists and returns its name.
6 ) The compare_xbrl function has been added. It compares the two output folders and determines if they can be merged.
7 ) The compare_filesizes function has been added. It compares the size of two files, and returns which one is bigger.
8 ) The clear_folder function has been added. It deletes any files in a folder.
9 ) The setup function is modified slightly so that it hooks into the main menu instead of the XBRL menu.
#include "XBRLMerger.rc" #define EXPORT_FOLDER_PREFIX "Merge" #define ERROR_WARN (ERROR_SOFT | 0x00005555) #define FILE_ONE 1 #define FILE_TWO 2 #define OTHER 999 string edit_windows[][]; int bigger_file; qword namesize_dif; string FileOne, FileTwo; handle FileOneWindow; handle FileTwoWindow; int run (int f_id, string mode); int validate_file (string file, string display); string export_file (string foldersuffix, string file, handle window); string get_export_folder (string file, string foldersuffix); int compare_xbrl (string instance,string f1folder, string f2folder); int clear_folder (string path); int compare_filesizes (string f1, string f2); /****************************************/ int setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* Us */ string item[10]; /* Menu Item */ int rc; /* Return Code */ /* */ /* ** Add Menu Item */ /* * Common */ item["Class"] = "Extension"; /* Function Code */ /* o Define Function */ item["Code"] = "XBRL_MERGE"; /* Function Code */ item["MenuText"] = "&Merge XFR Files"; /* Menu Text */ item["Description"] = "<B>Merge XBRL Files</B>"; /* description */ item["Description"].= "\r\rMerges two XBRL Instance Files."; /* description */ /* o Check for Existing */ rc = MenuFindFunctionID(item["Code"]); /* Look for existing */ if (IsNotError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ /* o Registration */ rc = MenuAddFunction(item); /* Add the item */ if (IsError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuSetHook(item["Code"], fnScript, "run"); /* Set the Hook */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */ /****************************************/ int run(f_id,mode){ /* main run loop */ /****************************************/ string errmsg; /* an error message */ string f1folder,f2folder; /* folders that were exported to */ string f1instance,f2instance; /* instance files of exported XFRs */ int rc; /* result */ /* */ if (mode!="preprocess"){ /* if not in preprocess */ return ERROR_NONE; /* return no error */ } /* */ edit_windows = EnumerateEditWindows(); /* get open edit windows */ rc = DialogBox("MergeXBRLDlg", "merge_"); /* open selector dialog */ if (IsError(rc)==true){ /* if the user didn't press OK */ CloseHandle(FileOneWindow); /* close handle */ CloseHandle(FileTwoWindow); /* close handle */ return rc; /* return */ } /* */ /* */ f1folder = get_export_folder(FileOne,"One"); /* get the file 1 folder */ errmsg = GetLastErrorMessage(); /* get the last error */ if (f1folder == "" ){ /* if we cannot get the folder */ MessageBox('x',"Cannot create folder for export. %s",errmsg); /* display error message */ return ERROR_EXIT; /* return with error */ } /* */ f2folder = get_export_folder(FileTwo,"Two"); /* get the file 2 folder */ errmsg = GetLastErrorMessage(); /* */ if (f2folder == ""){ /* if we cannot get a folder */ MessageBox('x',"Cannot create folder for export. %s",errmsg); /* display error message */ return ERROR_EXIT; /* return with error */ } /* */ f1instance = export_file("One",FileOne, FileOneWindow); /* export the first file */ f2instance = export_file("Two",FileTwo, FileTwoWindow); /* export the second file */ if (f1instance=="" || f2instance==""){ /* test if either instance is blank */ MessageBox('x',"Unable to export XFR files."); /* display error message */ return ERROR_EXIT; /* return with error */ } /* */ rc = compare_xbrl(f1instance,f1folder,f2folder); /* check if the files can be merged */ errmsg = GetLastErrorMessage(); /* get the last error message */ if (IsError(rc)){ /* if there was a problem */ if (rc==ERROR_WARN){ /* maybe not fatal, ask user to cont. */ rc = YesNoBox('q',"File size mismatch, result may have errors."+/* ask user */ " Continue?"); /* ask user */ if (rc!=IDYES){ /* if the user didn't press yes */ return ERROR_EXIT; /* return with error */ } /* */ } /* */ else{ /* if the error is definitely fatal */ MessageBox('x',"Files are not compatible to merge. %s", errmsg);/* display error */ return rc; /* return error code */ } /* */ } /* */ CloseHandle(FileOneWindow); /* close window handle */ CloseHandle(FileTwoWindow); /* close window handle */ return ERROR_NONE; } /****************************************/ int clear_folder(string path){ /* delete all files from given folder */ /****************************************/ string filenames[]; /* filenames to delete */ int num_files; /* number of files */ int rc; /* result */ int ix; /* counter */ /* */ path = AddPathDelimiter(path); /* ensure path ends in slash */ filenames = EnumerateFiles(path+"*.*"); /* get all files in folder */ num_files = ArrayGetAxisDepth(filenames); /* get the number of files in folder */ for (ix=0;ix<num_files;ix++){ /* for each file in folder */ rc = DeleteFile(AddPaths(path,filenames[ix])); /* delete it */ if (IsError(rc)){ /* if we couldn't delete it */ return rc; /* return the error code */ } /* */ } /* */ return ERROR_NONE; /* return no error */ } /****************************************/ int compare_filesizes(string f1, string f2){ /* compare two file sizes */ /****************************************/ qword f1size; /* file 1 size */ qword f2size; /* file 2 size */ int sizedif; /* difference in sizes */ /* */ f1size = GetFileSize(f1); /* get size of file 1 */ f2size = GetFileSize(f2); /* get size of file 2 */ sizedif = f1size-f2size; /* get difference in sizes */ if ((Absolute(sizedif) - namesize_dif) == 0){ /* if files are same sized */ return OTHER; /* return other */ } /* */ else{ /* if they are not the same size */ if (f1size>f2size){ /* if file one is bigger */ return FILE_ONE; /* return file one */ } /* otherwise */ return FILE_TWO; /* return file two */ } /* */ } /* */ /****************************************/ int compare_xbrl(string instance,string f1folder, string f2folder){ /* test if 2 XBRL files can be merged */ /****************************************/ /* */ int ix; /* loop counter */ int num_files; /* number of files in folder */ int sizeres; /* result of getting size dif */ string f1filepath; /* path to a file in folder 1 */ string f2filepath; /* path to a file in folder 2 */ string f1files[]; /* files in folders one */ /* */ f1folder = AddPathDelimiter(f1folder); /* ensure path ends in a slash */ f1files = EnumerateFiles(f1folder+"*.*"); /* get filenames in folder one */ /* */ num_files = ArrayGetAxisDepth(f1files); /* number of files in folder one */ for (ix=0;ix<num_files;ix++){ /* for each file in folder one */ f1filepath = AddPaths(f1folder,f1files[ix]); /* get path to file in folder one */ f2filepath = AddPaths(f2folder,f1files[ix]); /* get path to file in folder two */ if(IsFile(f2filepath)==false){ /* if this file doesn't exist in f2 */ SetLastError(ERROR_EXIT,"File "+f2filepath+" does not exist"); /* set error message */ return ERROR_EXIT; /* return with error */ } /* */ sizeres = compare_filesizes(f1filepath,f2filepath); /* get the file size result */ if(sizeres!=OTHER){ /* if the sizes don't match up */ if (f1files[ix]!=instance){ /* and this isn't the instance file */ bigger_file = sizeres; /* store the bigger file */ SetLastError(ERROR_WARN,"File "+f1files[ix]+" does not "+ /* set error */ "match." ); /* set error */ return ERROR_WARN; /* return error */ } /* */ } /* */ } /* */ return ERROR_NONE; /* return without error */ } /* */ /****************************************/ string export_file(string foldersuffix,string file, handle window){ /* export the XFR file */ /****************************************/ string path; /* path to output file */ string response; /* response from export */ string filenames[]; /* filenames */ string cmd; /* command to export */ int ix; /* counter */ int num_files; /* number of files */ /* */ path = get_export_folder(file,foldersuffix); /* get folder to export to */ if (path == ""){ /* if the export folder is blank */ return ""; /* return an error */ } /* */ path = AddPathDelimiter(path); /* ensure path ends in slash */ filenames = EnumerateFiles(path+"*.*"); /* get all files in folder */ num_files = ArrayGetAxisDepth(filenames); /* get the number of files in folder */ for (ix=0;ix<num_files;ix++){ /* for each file in folder */ DeleteFile(AddPaths(path,filenames[ix])); /* delete it */ } /* */ cmd = "NoQuery: TRUE; Path: " + path; /* generate command string */ if (IsWindowHandleValid(window)){ /* check if file is already open */ ActivateEditWindow(window); /* activate edit window */ RunMenuFunction("XBRL_EXPORT",cmd); /* export the file */ } /* */ else{ /* */ RunMenuFunction("FILE_OPEN","Filename:"+file); /* open file */ RunMenuFunction("XBRL_EXPORT",cmd); /* export the file */ } /* */ response = GetMenuFunctionResponse(); /* response from the export */ return GetParameter(response,"Instance"); /* return the name of the instance file */ } /****************************************/ string get_export_folder(string file, string foldersuffix){ /* build the path to the output folder */ /****************************************/ int rc; /* return code */ string folder; /* folder for XFR file */ string newfolder; /* new folder for exporting to */ /* */ folder = GetFilePath(file); /* get folder */ newfolder = EXPORT_FOLDER_PREFIX+foldersuffix; /* build name of new folder */ newfolder = AddPaths(folder,newfolder); /* build full path to newfolder */ if (IsFolder(newfolder)==false){ /* if folder doesn't exist */ rc = CreateFolder(newfolder); /* try to create the folder */ if (IsError(rc)){ /* did it create OK? */ return ""; /* return a blank string */ } /* */ } /* */ rc = clear_folder(newfolder); /* clear the export folder */ if (IsError(rc)){ /* if we couldn't clear the export */ SetLastError(rc,"Cannot delete files in folder "+newfolder); /* set error */ return ""; /* return */ } /* */ return newfolder; /* return the new folder path */ } /****************************************/ int main(){ /* main function */ /****************************************/ string s1; /* General */ /* */ s1 = GetScriptParent(); /* Get the parent */ if (s1 == "LegatoIDE") { /* Is run from the IDE (debug) */ setup(); /* run setup */ run(0,"preprocess"); /* run as though hooked */ } /* end IDE run */ } /* */ /****************************************/ int merge_load(){ /* Setup Action */ /****************************************/ string file_one,file_two; /* old file paths */ /* */ file_one = GetSetting("XBRLMerge","File One"); /* get path to file one */ file_two = GetSetting("XBRLMerge","File Two"); /* get path to file two */ /* */ EditSetText(XBRL_ONE_TEXT,file_one); /* set edit text */ EditSetText(XBRL_TWO_TEXT,file_two); /* set edit text */ } /****************************************/ int merge_action(int c_id, int c_ac) { /* Control Action */ /****************************************/ string s1; /* General */ /* */ /* ** Control Actions */ /* * Browse for XML 1 */ if (c_id == XBRL_RESET){ /* if resetting */ EditSetText(XBRL_ONE_TEXT,""); /* reset text of box */ PutSetting("XBRLMerge","File One",""); /* reset setting file */ EditSetText(XBRL_TWO_TEXT,""); /* reset text of box */ PutSetting("XBRLMerge","File Two",""); /* reset setting file */ } /* */ /* */ if (c_id == XBRL_ONE_BROWSE) { /* Control ID (button) */ s1 = EditGetText(XBRL_ONE_TEXT); /* Get the current path */ s1 = BrowseOpenFile("Select First XBRL File","*.xfr|*.xfr", s1); /* Browse for the folder */ if (s1 != "") { /* Returned a value (OK) */ EditSetText(XBRL_ONE_TEXT, s1); /* Get the current path */ PutSetting("XBRLMerge","File One",s1); /* store setting for later */ } /* end has string */ return ERROR_NONE; /* Done */ } /* end browse */ /* * Browse for XML 2 */ if (c_id == XBRL_TWO_BROWSE) { /* Control ID (button) */ s1 = EditGetText(XBRL_TWO_TEXT); /* Get the current path */ if (s1 == "") { /* Empty, pick up source */ s1 = EditGetText(XBRL_ONE_TEXT); /* Get the current path */ } /* end has string */ s1 = BrowseOpenFile("Select Second XBRL File","*.xfr|*.xfr",s1); /* Browse for the folder */ if (s1 != "") { /* Returned a value (OK) */ EditSetText(XML_TWO_TEXT, s1); /* Get the current path */ PutSetting("XBRLMerge","File Two",s1); /* store setting for later */ } /* end has string */ return ERROR_NONE; /* Done */ } /* end browse */ return ERROR_NONE; /* Exit no error */ } /* end routine */ /****************************************/ int merge_validate(){ /* Validate Action */ /****************************************/ int valid_one,valid_two; /* validations of files */ string file_one,file_two; /* file paths */ /* */ file_one = EditGetText(XBRL_ONE_TEXT); /* get path to file one */ file_two = EditGetText(XBRL_TWO_TEXT); /* get path to file two */ if (file_one == "" || file_two == ""){ /* if either file is blank */ MessageBox('x',"Two files must be selected."); /* make sure two files are selected */ return ERROR_EXIT; /* exit with error */ } /* */ if (file_one == file_two){ /* test if same file */ MessageBox('x',"You cannot merge a file into itself."); /* display error message */ return ERROR_EXIT; /* return with error */ } valid_one = validate_file(file_one,"One"); /* validate file_one */ if (IsWindowHandleValid(FileTwoWindow)){ /* if our validate set a file handle */ FileOneWindow = FileTwoWindow; /* store handle as file one */ FileTwoWindow = NULL_HANDLE; /* close file two window */ } /* */ valid_two = validate_file(file_two,"Two"); /* validate file_two */ if (valid_one==valid_two && valid_two==ERROR_NONE){ /* if both validates returned ERROR_NONE*/ return ERROR_NONE; /* return no error */ } /* */ return ERROR_EXIT; /* exit with an error */ } /****************************************/ int validate_file(string file, string display, handle file_handle){ /* validate an individual file */ /****************************************/ int rc; /* return code from our file */ int depth; /* number of windows open */ int ix; /* array index counter */ /* */ depth = ArrayGetAxisDepth(edit_windows); /* get number of windows open */ if (IsFile(file)){ /* check if file one is a file */ if (CanAccessFile(file,FO_WRITE)){ /* check if we can write to the file */ if (MakeLowerCase(GetExtension(file))==".xfr"){ /* make sure file ends in .xfr */ return ERROR_NONE; /* return no error */ } /* */ else{ /* if file doesn't end in .xfr */ MessageBox('x',"File "+display+" Must be an XFR file."); /* display error message */ } /* */ } /* */ else{ /* if we cannot write to file */ for (ix = 0; ix<depth; ix++){ /* scan all open windows */ if (edit_windows[ix]["Filename"] == file){ /* if the file is already open */ FileTwoWindow=MakeHandle(edit_windows[ix]["ClientHandle"]); /* get the handle to it */ rc = GetLastError(); /* check if we got an error */ if (IsError(rc)){ /* if we have an error */ MessageBox('x',"Cannot create handle, error %0x",rc); /* display error message */ } /* */ else{ /* if we don't have an error */ return ERROR_NONE; /* the file is already open, return */ } /* */ } /* */ } /* */ MessageBox('x',"Cannot open File "+display); /* give error message */ } /* */ } /* */ else{ /* if we cannot open file one */ MessageBox('x',"File "+display+" does not exist."); /* give error message */ } /* if we haven't exited yet */ return ERROR_EXIT; /* return an error */ } /* */ /****************************************/ int merge_ok(){ /* OK Action */ /****************************************/ int f1namesize,f2namesize; /* sizes of the names of xfr files */ string f1,f2; /* file names chosen by user */ /* */ FileOne = EditGetText(XBRL_ONE_TEXT); /* get path to file one */ FileTwo = EditGetText(XBRL_TWO_TEXT); /* get path to file two */ /* */ f1 = GetFilename(FileOne); /* get the name of the first xfr file */ f2 = GetFilename(FileTwo); /* get the name of the second xfr file */ f1namesize = GetStringLength(EncodeURIComponent(f1)); /* size of file 1 name */ f2namesize = GetStringLength(EncodeURIComponent(f2)); /* size of file 2 name */ namesize_dif = Absolute(f1namesize-f2namesize); /* get size of dif in name sizes */ return ERROR_NONE; /* return that the user pressed OK */ }
These code walkthrough paragraphs are going to be presented in the order I think makes the most sense for understanding the overall application, not necessarily the order they appear in in the actual script file. The order in the file is pretty much irrelevant, as they are only run when called.
We’ve added a couple of new defines at the top, EXPORT_FOLDER_PREFIX is going to be used as part of the folder name for the exported file sets. Putting it at the top as a define is a good idea in case we want to change it later; we can just edit the value here instead of finding the locations in the code. ERROR_WARN is a new return code we will be using when we need something between ERROR_NONE and ERROR_EXIT. In this case, should that error occur, the script asks the user if he/she wants to continue with files that may not correctly merge. FileOne and FileTwo are global strings that hold the path to the file that will be exported. They should be set by the merge_ok function when the user presses OK on the dialog. Also set by the merge_ok function is namesize_dif, which is the difference in the size of the selected file’s names. We need this for comparing files. Lastly, we have three defines, FILE_ONE, FILE_TWO, and OTHER, which are returned by our compare_filesizes function.
#include "XBRLMerger.rc" #define EXPORT_FOLDER_PREFIX "Merge" #define ERROR_WARN (ERROR_SOFT | 0x00005555) #define FILE_ONE 1 #define FILE_TWO 2 #define OTHER 999 string edit_windows[][]; int bigger_file; qword namesize_dif; string FileOne, FileTwo; handle FileOneWindow; handle FileTwoWindow;
The setup function has very few actual changes. The item variable’s value for “Class” has been set to “Extension” instead of “XBRLExtension” so this script can be run off the tools menu on the file toolbar. This may be a better choice for accessibility. Also, the MenuSetHook SDK function now properly hooks onto the run function.
/****************************************/ int setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* Us */ string item[10]; /* Menu Item */ int rc; /* Return Code */ /* */ /* ** Add Menu Item */ /* * Common */ item["Class"] = "Extension"; /* Function Code */ /* o Define Function */ item["Code"] = "XBRL_MERGE"; /* Function Code */ item["MenuText"] = "&Merge XFR Files"; /* Menu Text */ item["Description"] = "<B>Merge XBRL Files</B>"; /* description */ item["Description"].= "\r\rMerges two XBRL Instance Files."; /* description */ /* o Check for Existing */ rc = MenuFindFunctionID(item["Code"]); /* Look for existing */ if (IsNotError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ /* o Registration */ rc = MenuAddFunction(item); /* Add the item */ if (IsError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ fnScript = GetScriptFilename(); /* Get the script filename */ MenuSetHook(item["Code"], fnScript, "run"); /* Set the Hook */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */
The merge_ok function has been modified so that it stores the information from the XBRL_ONE_TEXT and XBRL_TWO_TEXT text fields on the dialog in our FileOne and FileTwo global variables, so the rest of the script can access the names of what files the user has chosen. Then, it gets the filenames of FileOne and FileTwo using the GetFilename function, and gets the size of those filenames with the GetStringLength function. We care about the filename in an encoded format, so we’ll need to use the EncodeURIComponent function to encode our filenames first though. Once we have the size of each encoded filename, we can calculate the difference in size between them by subtracting one from the other and taking the absolute value with the Absolute function. We will need this value later.
/****************************************/ int merge_ok(){ /* OK Action */ /****************************************/ int f1namesize,f2namesize; /* sizes of the names of xfr files */ string f1,f2; /* file names chosen by user */ /* */ FileOne = EditGetText(XBRL_ONE_TEXT); /* get path to file one */ FileTwo = EditGetText(XBRL_TWO_TEXT); /* get path to file two */ /* */ f1 = GetFilename(FileOne); /* get the name of the first xfr file */ f2 = GetFilename(FileTwo); /* get the name of the second xfr file */ f1namesize = GetStringLength(EncodeURIComponent(f1)); /* size of file 1 name */ f2namesize = GetStringLength(EncodeURIComponent(f2)); /* size of file 2 name */ namesize_dif = Absolute(f1namesize-f2namesize); /* get size of dif in name sizes */ return ERROR_NONE; /* return that the user pressed OK */ }
The first new function this week, clear_folder, is a pretty basic subroutine. It’s task is to take a path as a parameter, use the EnumerateFiles function to get a list of all files in that folder. This function requires the path to end in the wildcard *.*, so it will match every file in it. The wildcard can be modified if you’re looking for specific file types, but we want every file, so *.* works. Using that, we get an array of filenames we can loop over with a for loop, and run the DeleteFile function on each one. If for any reason the DeleteFile function returns an error code, we can return the error code to the function that is calling this routine. Otherwise, we can continue the loop, and eventually return without error.
/****************************************/ int clear_folder(string path){ /* delete all files from given folder */ /****************************************/ string filenames[]; /* filenames to delete */ int num_files; /* number of files */ int rc; /* result */ int ix; /* counter */ /* */ path = AddPathDelimiter(path); /* ensure path ends in slash */ filenames = EnumerateFiles(path+"*.*"); /* get all files in folder */ num_files = ArrayGetAxisDepth(filenames); /* get the number of files in folder */ for (ix=0;ix<num_files;ix++){ /* for each file in folder */ rc = DeleteFile(AddPaths(path,filenames[ix])); /* delete it */ if (IsError(rc)){ /* if we couldn't delete it */ return rc; /* return the error code */ } /* */ } /* */ return ERROR_NONE; /* return no error */ }
The next new function for this week, get_export_folder, takes as parameters the file we are going to export and a suffix we will add to our defined prefix to make a complete output folder name. The first thing this function does with these parameters is use the GetFilePath SDK function to trim the file name off the output file, so we have a path to the folder in which it’s located. Then we can add our defined EXPORT_FOLDER_PREFIX to our XBRL instance parameter foldersuffix to get our output folder, and we can use the AddPaths function to concatenate the two. Once we have the folder name, the IsFolder function can check if it’s a folder that actually exists. If not, our script will create it with the CreateFolder function. The result is checked with the IsError function, and if it’s an error,we return a blank string. Otherwise, we can continue, and run our clear_folder function. It will make sure any files in our export folder left over from a previous merge are deleted. We need to check if this function returned an error with the IsError function afterwards, because if we weren’t able to delete a file it means we cannot export to the directory. If there was an error, we can set an error code and return a blank string. Otherwise, we can continue, and return the path of the export folder.
/****************************************/ string get_export_folder(string file, string foldersuffix){ /* build the path to the output folder */ /****************************************/ int rc; /* return code */ string folder; /* folder for XFR file */ string newfolder; /* new folder for exporting to */ /* */ folder = GetFilePath(file); /* get folder */ newfolder = EXPORT_FOLDER_PREFIX+foldersuffix; /* build name of new folder */ newfolder = AddPaths(folder,newfolder); /* build full path to newfolder */ if (IsFolder(newfolder)==false){ /* if folder doesn't exist */ rc = CreateFolder(newfolder); /* try to create the folder */ if (IsError(rc)){ /* did it create OK? */ return ""; /* return a blank string */ } /* */ } /* */ rc = clear_folder(newfolder); /* clear the export folder */ if (IsError(rc)){ /* if we couldn't clear the export */ SetLastError(rc,"Cannot delete files in folder "+newfolder); /* set error */ return ""; /* return */ } /* */ return newfolder; /* return the new folder path */ }
The function export_file does exactly what you’d expect. It exports an XFR file to a target output folder in the same directory. The parameter foldersuffix is the suffix that will be used in the above function, get_export_folder, when it’s called inside this function. The parameter file is the path to the file that we will be exporting. The parameter window is the handle to the open XFR file window if it’s already open in GoFiler. First, we need to use our get_export_folder function to define our XBRL instance variable path, so we know the location of the exported file set. If this function returned a blank string, then it means our file cannot be exported, so we should just return here.
/****************************************/ string export_file(string foldersuffix,string file, handle window){ /* export the XFR file */ /****************************************/ string path; /* path to output file */ string response; /* response from export */ string filenames[]; /* filenames */ string cmd; /* command to export */ int ix; /* counter */ int num_files; /* number of files */ /* */ path = get_export_folder(file,foldersuffix); /* get folder to export to */ if (path == ""){ /* if the export folder is blank */ return ""; /* return an error */ } /* */ path = AddPathDelimiter(path); /* ensure path ends in slash */
Now that we have our output path set up, we can build our command string, cmd. These are the extra parameters sent to the XBRL_EXPORT function, telling it to not query us for information and to use our output folder instead of the same folder as the XFR file. Next, we can check if we were actually passed a valid file handle to a currently open XFR file. If so, we need to activate that window with the ActivateEditWindow function. Then we use the RunMenuFunction function to run the XBRL_EXPORT function with our command string. This will export our files for us. If the handle passed was not valid, it means the file isn’t already open. In this case, we need to use the RunMenuFunction function to run the FILE_OPEN function, which opens up our file. Then we can use the XBRL_EXPORT function to export it. Lastly, we need to run the GetMenuFunctionResponse function to get the results of our actions, and return the instance parameter, which should be the name of the XBRL instance file that was exported.
cmd = "NoQuery: TRUE; Path: " + path; /* generate command string */ if (IsWindowHandleValid(window)){ /* check if file is already open */ ActivateEditWindow(window); /* activate edit window */ RunMenuFunction("XBRL_EXPORT",cmd); /* export the file */ } /* */ else{ /* */ RunMenuFunction("FILE_OPEN","Filename:"+file); /* open file */ RunMenuFunction("XBRL_EXPORT",cmd); /* export the file */ } /* */ response = GetMenuFunctionResponse(); /* response from the export */ return GetParameter(response,"Instance"); /* return the name of the instance file */ }
The next function we’re adding is compare_filesizes. We need this because determining if files are bigger or smaller is going to help us determine if we can merge them or not, and will eventually be used to determine which file is going to merge into the other (the smaller merges into the bigger). First, we can use the GetFileSize function to get the size of both files. Next we can subtract these values to get the difference in size. Then, take the absolute value of difference, and subtract the variable namesize_dif that the merge_ok function set. If the result is zero, the files are equal, so we can return the constant OTHER. We need to subtract the namesize_dif value because inside each file exported, the name of the source XFR is embedded as a comment. We really only care if the contents of the files are different, not the comments on the files. So if we correct for this known difference, we can use the filesize to check if the files are the same. This doesn’t guarantee that the files are the same though, even if the file sizes are identical (for example, 123 and 456 are different values but would have the same file size in an exported XML file) but it’s close enough for our purposes here.
If the difference in file sizes after correcting for the filename isn’t zero, then we can compare the size of file one to file two, and if file one is bigger, return the constant FILE_ONE. Otherwise, we can return FILE_TWO.
/****************************************/ int compare_filesizes(string f1, string f2){ /* compare two file sizes */ /****************************************/ qword f1size; /* file 1 size */ qword f2size; /* file 2 size */ int sizedif; /* difference in sizes */ /* */ f1size = GetFileSize(f1); /* get size of file 1 */ f2size = GetFileSize(f2); /* get size of file 2 */ sizedif = f1size-f2size; /* get difference in sizes */ if ((Absolute(sizedif) - namesize_dif) == 0){ /* if files are same sized */ return OTHER; /* return other */ } /* */ else{ /* if they are not the same size */ if (f1size>f2size){ /* if file one is bigger */ return FILE_ONE; /* return file one */ } /* otherwise */ return FILE_TWO; /* return file two */ } /* */ } /* */
The last new function we’ve added to the script is the compare_xbrl function. This function takes the name of an instance file as a parameter, as well as the two output folders to which we will export using the export_file function. This function’s purpose is to check if the folders have similar enough contents to merge. Since the only things that should be different are the instance, they must have the same files in them, and they should have the same sizes in each of those files.
First, we need to use the EnumerateFiles function the same way it was used in the clear_folder function to get a list of all the files in file one’s folder. Then, we can iterate over each file in that folder, and, using its filename and the f1folder and f2folder parameters, we can build complete paths to the files in folder one and folder two. Now we know the file in folder one exists (because we’re iterating over its contents), but we need to ensure the same file exists in folder two, so we check it using the IsFile function. If the file isn’t there, use the SetLastError function to set an error message and return ERROR_EXIT to stop execution. If there is a file, we can move on, and use the compare_filesizes function to compare our file in folder one with our file in folder two of the same name. If this function returns the constant OTHER, great, we can keep going. If there is a mismatch, and one is bigger than the other, we need to check if we’re dealing with an instance file, because those are supposed to be different sizes, as they have different facts. If we are, we can continue. If not, then it means one of the linkbase files is a different size than the other. This signifies a potential merging problem. It might also be expected, if one of the files was modified after the other was created. So we can use the SetLastError function to set a message, and here is where we want to return ERROR_WARN, which can be handled by our run function by asking the user if he/she wants to continue.
/****************************************/ int compare_xbrl(string instance,string f1folder, string f2folder){ /* test if 2 XBRL files can be merged */ /****************************************/ /* */ int ix; /* loop counter */ int num_files; /* number of files in folder */ int sizeres; /* result of getting size dif */ string f1filepath; /* path to a file in folder 1 */ string f2filepath; /* path to a file in folder 2 */ string f1files[]; /* files in folders one */ /* */ f1folder = AddPathDelimiter(f1folder); /* ensure path ends in a slash */ f1files = EnumerateFiles(f1folder+"*.*"); /* get filenames in folder one */ /* */ num_files = ArrayGetAxisDepth(f1files); /* number of files in folder one */ for (ix=0;ix<num_files;ix++){ /* for each file in folder one */ f1filepath = AddPaths(f1folder,f1files[ix]); /* get path to file in folder one */ f2filepath = AddPaths(f2folder,f1files[ix]); /* get path to file in folder two */ if(IsFile(f2filepath)==false){ /* if this file doesn't exist in f2 */ SetLastError(ERROR_EXIT,"File "+f2filepath+" does not exist"); /* set error message */ return ERROR_EXIT; /* return with error */ } /* */ sizeres = compare_filesizes(f1filepath,f2filepath); /* get the file size result */ if(sizeres!=OTHER){ /* if the sizes don't match up */ if (f1files[ix]!=instance){ /* and this isn't the instance file */ bigger_file = sizeres; /* store the bigger file */ SetLastError(ERROR_WARN,"File "+f1files[ix]+" does not "+ /* set error */ "match." ); /* set error */ return ERROR_WARN; /* return error */ } /* */ } /* */ } /* */ return ERROR_NONE; /* return without error */ } /* */
The run function has been pretty heavily modified. Instead of just opening the dialog, it now checks the variable rc that contains the return code from our dialog, which should be ERROR_NONE unless there was an error. If rc is anything else it means the dialog failed to open due to an error or the user pressed cancel. Either way, we can close our file handles and simply return. Otherwise, we use our get_export_folder function to get our export destinations for file one and file two. If either of them is blank, it means we cannot export, so we can display an error message and return. Then we can use our export_file function to actually export the files, and get the resulting instance files. If either of those instance files is blank as a result, it means the export failed, so again we can show an error message and return. Otherwise, we can run the compare_xbrl function, which compares the two exported file sets and checks if they can be combined. Using GetLastErrorMessage, we can check the result message set by this function. If the return code is anything other than ERROR_NONE, it means we have a problem and have to display it to the user. If the message was ERROR_WARN, it means the problem may not be fatal, so we should ask the user if we want to continue by opening a YesNoBox and taking the response code from it. In that case, if the response from that function is anything besides IDYES, it means the user didn’t click yes, so we can return. If the return code from the compare_xbrl function wasn’t ERROR_WARN or ERROR_NONE, there was a fatal error, so we need to display the message to the user with the MessageBox and then return.
/****************************************/ int run(f_id,mode){ /* main run loop */ /****************************************/ string errmsg; /* an error message */ string f1folder,f2folder; /* folders that were exported to */ string f1instance,f2instance; /* instance files of exported XFRs */ int rc; /* result */ /* */ if (mode!="preprocess"){ /* if not in preprocess */ return ERROR_NONE; /* return no error */ } /* */ edit_windows = EnumerateEditWindows(); /* get open edit windows */ rc = DialogBox("MergeXBRLDlg", "merge_"); /* open selector dialog */ if (IsError(rc)){ /* if the user didn't press OK */ CloseHandle(FileOneWindow); /* close handle */ CloseHandle(FileTwoWindow); /* close handle */ return rc; /* return */ } /* */ /* */ f1folder = get_export_folder(FileOne,"One"); /* get the file 1 folder */ f2folder = get_export_folder(FileTwo,"Two"); /* get the file 2 folder */ if (f1folder == "" || f2folder == ""){ /* if we cannot get a folder */ MessageBox('x',"Cannot create folder for export."); /* display error message */ return ERROR_EXIT; /* return with error */ } /* */ f1instance = export_file("One",FileOne, FileOneWindow); /* export the first file */ f2instance = export_file("Two",FileTwo, FileTwoWindow); /* export the second file */ if (f1instance=="" || f2instance==""){ /* test if either instance is blank */ MessageBox('x',"Unable to export XFR files."); /* display error message */ return ERROR_EXIT; /* return with error */ } /* */ rc = compare_xbrl(f1instance,f1folder,f2folder); /* check if the files can be merged */ errmsg = GetLastErrorMessage(); /* get the last error message */ if (IsError(rc)){ /* if there was a problem */ if (rc==ERROR_WARN){ /* maybe not fatal, ask user to cont. */ rc = YesNoBox('q',"File size mismatch, result may have errors."+/* ask user */ " Continue?"); /* ask user */ if (rc!=IDYES){ /* if the user didn't press yes */ return ERROR_EXIT; /* return with error */ } /* */ } /* */ else{ /* if the error is definitely fatal */ MessageBox('x',"Files are not compatible to merge. %s", errmsg);/* display error */ return rc; /* return error code */ } /* */ } /* */ CloseHandle(FileOneWindow); /* close window handle */ CloseHandle(FileTwoWindow); /* close window handle */ return ERROR_NONE; }
So we are getting closer and closer to an actual usable script. Now it asks for input, validates the input, and tries to export XBRL files, while handling potential errors and problems along the way in a fairly user-friendly manner rather than just quitting. Next week, we will actually read the instance files, parse their contents into data structures, and begin comparing the contents of the files. This will allow us to actually write out a complete merged instance file in the following week.
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
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato