New rules go into effect next month requiring that filers insert hyperlinks into their exhibit indices to link directly to referenced filings and exhibits. For a small filing, maybe this isn’t too arduous a task to do by hand. But for a large one? The process can be a real pain, particularly if you don’t know the links of the exact exhibits that need to be included. GoFiler has now included a tool (written entirely in Legato by our development staff!) to help users locate and automatically include exhibits. It’s pretty powerful, allowing the user to build a local cache of filings from the EDGAR system based on a particular CIK. This cache can be searched and sorted, so it becomes much easier to find the filing you need to link.
Friday, September 01. 2017
LDC #50: Making Linking Exhibits Even Easier
Today’s script makes it even easier. We’re going to expand on the Exhibit Linker tool to allow the user to set a default CIK and choose a cache location. Normally the cache defaults to the application’s local data directory, but this path can be changed (though not in a user-friendly way without this script). The user can also look up any CIK with the Exhibit Linker, but it would be convenient to set a default (for example, the most commonly used CIK for a production team). Our script also makes sure the CIK provided is valid. This can be pretty handy to build a library of related filings for a particular project. Let’s take a look.
// // // GoFiler Legato Script - Edit Exhibit Linker Options // ----------------------------------------------- // // Rev 08/30/2017 // // (c) 2017 Novaworks, LLC -- All rights reserved. // // Allows you to edit the INI settings for the Exhibit Linker Script #define INVALID_CIK "CIK %s is invalid. Please enter a valid CIK." #define INVALID_FOLDER "Folder '%s' is invalid. Please enter a valid folder." #define LINK_INI GetApplicationDataLocalFolder()+"HTMLExhibitLinker.ini" #include "ExhibitOptions.rc" int run (int f_id, string mode); /* run menu */ boolean is_valid_cik (); /* check if CIK is valid */ /****************************************/ int setup() { /* Called from hook processor */ /****************************************/ string fnScript; /* Us */ string item[10]; /* Menu Item */ int rc; /* Return Code */ /* */ /* ** Add Menu Item */ /* * Define Function */ item["Code"] = "EXHIBIT_LINKER_OPTIONS"; /* Function Code */ item["MenuText"] = "&Exhibit Linker Options"; /* Menu Text */ item["Description"] = "<B>Exhibit Linker Options</B>"; /* Description (long) */ item["Description"]+= "\r\rEdit Previewer Options"; /* * */ item["SmallBitmap"] = "HTML_LINKER_OPTIONS"; /* set image icon */ rc = MenuFindFunctionID(item["Code"]); /* Look for existing */ if (IsNotError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ /* * 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 Test Hook */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */ /****************************************/ int options_load(){ /* load */ /****************************************/ /* */ string cik, loc; /* stored values */ /* */ cik = GetSetting(LINK_INI,"Filter Setting", "CIK"); /* Get CIK */ loc = GetSetting(LINK_INI,"SEC Exhibit Linker", /* Get cache location */ "EDGAR Cache Location"); /* Get cache location */ if (loc==""){ /* if loc is blank */ loc = "(default)"; /* set loc */ } /* */ EditSetText(DEFAULT_CIK,cik); /* set cik */ EditSetText(CACHE_LOCATION,loc); /* set cache */ return ERROR_NONE; /* return no error */ } /****************************************/ int options_validate(){ /* validate */ /****************************************/ int rc; /* return code */ string cik, loc; /* stored values */ string cik_info[]; /* info about a cik */ /* */ cik = EditGetText(DEFAULT_CIK); /* get CIK from options */ loc = EditGetText(CACHE_LOCATION); /* get Cache from options */ /* */ if (cik!=""){ /* if a CIK was entered */ cik_info = EDGARLookupCIK(cik); /* get CIK info */ if (ArrayGetAxisDepth(cik_info)==0){ /* if response is empty */ MessageBox('x',INVALID_CIK,cik); /* display error */ return ERROR_EXIT; /* return back to menu */ } /* */ } /* */ if (IsFolder(loc)==false){ /* if it's not a folder */ rc = CreateFolder(loc); /* try to create it. */ if (IsError(rc)){ /* if we cannot */ MessageBox('x',INVALID_FOLDER,loc); /* display error */ return ERROR_EXIT; /* return back to menu */ } } /* */ if (loc != "(default)"){ /* if not a default location */ PutSetting(LINK_INI,"SEC Exhibit Linker", /* set ini output */ "EDGAR Cache Location",loc); } /* */ PutSetting(LINK_INI,"Filter Setting","CIK",cik); /* set ini output */ return ERROR_NONE; /* Return value (does not matter) */ } /****************************************/ int run(int f_id, string mode){ /* run function */ /****************************************/ if (mode!="preprocess"){ /* if only preprocess */ return ERROR_EXIT; /* return error */ } /* */ return DialogBox(LINKER_PROPERTIES, "options_"); /* open the dialog */ } /* */ int main(){ setup(); return ERROR_NONE; /* Return value (does not matter) */ } #beginresource HTML_LINKER_OPTIONS BITMAP { '42 4D 78 06 00 00 00 00 00 00 36 00 00 00 28 00' '00 00 14 00 00 00 14 00 00 00 01 00 20 00 00 00' '00 00 42 06 00 00 12 0B 00 00 12 0B 00 00 00 00' '00 00 00 00 00 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 63 3D 26 00 63 3D 26 00 63 3D 26 00 63 3D' '26 00 CC 00 FF 00 CC 00 FF 00 63 3D 26 00 63 3D' '26 00 63 3D 26 00 63 3D 26 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 63 3D' '26 00 D0 B0 A0 00 E0 D0 C0 00 E0 C8 B0 00 D0 C0' 'B0 00 63 3D 26 00 63 3D 26 00 B0 90 80 00 E0 C8' 'B0 00 E0 D0 C0 00 D0 B0 A0 00 63 3D 26 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 60 48 30 00 60 48 30 00 63 3D 26 00 E0 D0' 'C0 00 B3 95 85 00 A1 83 65 00 A1 83 65 00 50 28' '10 00 40 28 10 00 40 28 10 00 50 28 10 00 A1 83' '65 00 50 68 D0 00 10 28 90 00 50 68 D0 00 63 3D' '26 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 FF FF FF 00 B0 A0 90 00 63 3D 26 00 FF FF' 'FF 00 63 3D 26 00 D2 BB AA 00 D5 B7 A7 00 FF FF' 'FF 00 FF E8 E0 00 FF F8 F0 00 FF FF FF 00 D5 B7' 'A7 00 00 30 D0 00 10 48 FF 00 10 28 90 00 63 3D' '26 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 FF FF FF 00 FF FF FF 00 63 3D 26 00 F0 E0' 'D0 00 9B 8A 7A 00 59 39 29 00 60 48 30 00 B0 98' '80 00 C0 A8 90 00 D0 B0 A0 00 B0 98 80 00 60 48' '30 00 50 68 D0 00 00 30 D0 00 50 68 D0 00 63 3D' '26 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 FF FF FF 00 FF FF FF 00 FF FF FF 00 63 3D' '26 00 E0 D8 D0 00 FF F8 F0 00 FF F0 F0 00 D0 C0' 'B0 00 63 3D 26 00 63 3D 26 00 D0 C0 B0 00 FF F0' 'F0 00 FF F8 F0 00 60 48 30 00 63 3D 26 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 FF FF FF 00 FF FF FF 00 FF FF FF 00 FF FF' 'FF 00 63 3D 26 00 63 3D 26 00 63 3D 26 00 63 3D' '26 00 B0 A0 90 00 60 48 30 00 63 3D 26 00 63 3D' '26 00 63 3D 26 00 40 50 B0 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 FF FF FF 00 FF FF FF 00 FF FF FF 00 FF FF' 'FF 00 FF FF FF 00 F0 E8 E0 00 F0 E0 E0 00 F0 D8' 'D0 00 B0 A0 90 00 60 48 30 00 CC 00 FF 00 CC 00' 'FF 00 50 68 D0 00 00 38 F0 00 50 68 D0 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 FF FF FF 00 FF FF FF 00 FF FF FF 00 FF FF' 'FF 00 FF FF FF 00 FF F8 F0 00 F0 E8 E0 00 F0 E0' 'E0 00 B0 A0 90 00 60 48 30 00 CC 00 FF 00 CC 00' 'FF 00 20 40 C0 00 00 38 F0 00 0E 32 93 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 FF FF FF 00 FF FF FF 00 FF FF FF 00 FF FF' 'FF 00 FF FF FF 00 FF FF FF 00 FF FF FF 00 F0 E8' 'E0 00 B0 A0 90 00 60 48 30 00 CC 00 FF 00 CC 00' 'FF 00 50 70 E0 00 00 40 FF 00 00 30 D0 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 FF FF FF 00 FF FF FF 00 FF FF FF 00 FF FF' 'FF 00 FF FF FF 00 FF FF FF 00 FF F8 F0 00 F0 F0' 'F0 00 B0 A0 90 00 60 48 30 00 CC 00 FF 00 CC 00' 'FF 00 50 78 E0 00 10 48 FF 00 00 40 F0 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 FF FF FF 00 FF FF FF 00 FF FF FF 00 FF FF' 'FF 00 FF FF FF 00 FF FF FF 00 FF FF FF 00 B0 A0' '90 00 B0 A0 90 00 60 48 30 00 CC 00 FF 00 50 68' 'D0 00 70 90 FF 00 10 50 FF 00 10 40 F0 00 0E 32' 'A2 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 FF FF FF 00 FF FF FF 00 FF FF FF 00 FF FF' 'FF 00 FF FF FF 00 FF FF FF 00 B0 A0 90 00 60 48' '30 00 60 48 30 00 60 48 30 00 CC 00 FF 00 60 78' 'D0 00 80 98 FF 00 30 60 FF 00 10 50 FF 00 1D 41' 'C0 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 FF FF FF 00 FF FF FF 00 FF FF FF 00 FF FF' 'FF 00 FF FF FF 00 FF FF FF 00 C0 A8 90 00 D0 C8' 'C0 00 60 48 30 00 B0 A0 90 00 CC 00 FF 00 70 88' 'E0 00 90 A8 F0 00 80 A0 FF 00 60 80 F0 00 39 52' 'A5 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 F0 FF FF 00 FF FF FF 00 FF FF FF 00 FF FF' 'FF 00 FF FF FF 00 FF FF FF 00 C0 A8 A0 00 60 48' '30 00 B0 A0 90 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 70 88 E0 00 60 78 D0 00 50 68 D0 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 B0 A0' '90 00 B0 A0 90 00 B0 A0 90 00 B0 A0 90 00 B0 A0' '90 00 D0 B8 B0 00 D0 B8 B0 00 D0 B0 A0 00 B0 A0' '90 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 CC 00 FF 00 CC 00 FF 00 CC 00' 'FF 00 CC 00 FF 00 00 00 ' } #endresource
Our script begins with a few defines that contain messages to the user as well as a link to the application’s INI file, which we’ll need to adjust the default location of the Exhibit Linker’s cache and the default CIK. We also include a resource file, the contents of which appear below:
#define LINKER_PROPERTIES 2 #define DEFAULT_CIK 102 #define CACHE_LOCATION 101 LINKER_PROPERTIES DIALOGEX 0, 0, 240, 84 EXSTYLE WS_EX_DLGMODALFRAME | WS_EX_CONTEXTHELP STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Exhibit Linker Options" FONT 8, "MS Sans Serif" { CONTROL "OK", IDOK, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 132, 65, 50, 14 CONTROL "Cancel", IDCANCEL, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 187, 65, 50, 14 CONTROL "Default CIK:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 14, 25, 40, 13, 0 CONTROL "Cache Location:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 14, 44, 53, 13, 0 CONTROL "Edit Exhibit Linker Options:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 10, 8, 86, 13, 0 CONTROL "Frame1", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 100, 12, 130, 1, 0 CONTROL "", DEFAULT_CIK, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 73, 25, 152, 12, 0 CONTROL "", CACHE_LOCATION, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 73, 43, 152, 12, 0 }
We define a dialog we call LINKER_PROPERTIES, which contains some static, button, and edit controls. We’ve covered how to develop dialogs in Legato in previous blog posts. This dialog is going to allow the user to specify the default CIK and where its cache should be stored. You should also note that we have another resource defined in the script file itself. It’s nested inside the #beginresource and #endresource key words. Resources can be defined in either manner, in a separate file or within these keywords inside the script file. In this case, we’re defining a bitmap to be used as a menu icon inside the script file. We’re defining the image itself using hexadecimal. For more information on creating and using menu icons, see last week’s blog post.
Now that we have our resources defined, let’s examine the main script. Our functions include the usual setup, main, and run functions, as well as some event handlers for our dialog that begin with the prefix options_. The main function simply calls the setup function, which is as follows:
/****************************************/ int setup() { /* Called from hook processor */ /****************************************/ string fnScript; /* Us */ string item[10]; /* Menu Item */ int rc; /* Return Code */ /* */ /* ** Add Menu Item */ /* * Define Function */ item["Code"] = "EXHIBIT_LINKER_OPTIONS"; /* Function Code */ item["MenuText"] = "&Exhibit Linker Options"; /* Menu Text */ item["Description"] = "<B>Exhibit Linker Options</B>"; /* Description (long) */ item["Description"]+= "\r\rEdit Previewer Options"; /* * */ item["SmallBitmap"] = "HTML_LINKER_OPTIONS"; /* set image icon */ rc = MenuFindFunctionID(item["Code"]); /* Look for existing */ if (IsNotError(rc)) { /* Was already be added */ return ERROR_NONE; /* Exit */ } /* end error */ /* * 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 Test Hook */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */
As we’ve covered before, our setup function adds our script to the menu as a menu item. We’ve covered this procedure multiple times before. Briefly, we define our menu item’s parameters in the item array, which has specific keys. Of note, this time we define the “SmallBitmap” key to point to our HTML_LINKER_OPTIONS bitmap defined in our resource section.
Once we have the parameters set, we use the MenuAddFunction function, and we set our hook with the MenuSetHook function to the run function. The run function is as follows:
/****************************************/ int run(int f_id, string mode){ /* run function */ /****************************************/ if (mode!="preprocess"){ /* if only preprocess */ return ERROR_EXIT; /* return error */ } /* */ return DialogBox(LINKER_PROPERTIES, "options_"); /* open the dialog */ } /* */
The run function is pretty simple this week. We simply exit if the mode parameter is not preprocess, and then we open our dialog using the DialogBox function. We specify our LINKER_PROPERTIES dialog and link functions with the options_ prefix to the dialog’s events. This now displays the dialog to the user. Let’s see what happens when the user interacts with it. We have two event handlers defined: options_load and options_validate.
/****************************************/ int options_load(){ /* load */ /****************************************/ /* */ string cik, loc; /* stored values */ /* */ cik = GetSetting(LINK_INI,"Filter Setting", "CIK"); /* Get CIK */ loc = GetSetting(LINK_INI,"SEC Exhibit Linker", /* Get cache location */ "EDGAR Cache Location"); /* Get cache location */ if (loc==""){ /* if loc is blank */ loc = "(default)"; /* set loc */ } /* */ EditSetText(DEFAULT_CIK,cik); /* set cik */ EditSetText(CACHE_LOCATION,loc); /* set cache */ return ERROR_NONE; /* return no error */ }
This is the options_load function. As you might expect, it simply loads what the default CIK and default cache location currently are into the edit fields. It obtains what these values are from the INI file using the GetSetting function. INI entries are stored in sections, so you need to know both the entry name and the section name to retrieve the value (see the Legato SDK for more information). In this case, we read the CIK and the EDGAR Cache Location. If the location is empty, the application is using the default location, and we indicate that. Then we load both the values into their respective edit fields with the EditSetText function.
Now the user can see and change the data. Once he or she presses OK, our options_validate function is called.
/****************************************/ int options_validate(){ /* validate */ /****************************************/ int rc; /* return code */ string cik, loc; /* stored values */ string cik_info[]; /* info about a cik */ /* */ cik = EditGetText(DEFAULT_CIK); /* get CIK from options */ loc = EditGetText(CACHE_LOCATION); /* get Cache from options */ /* */ if (cik!=""){ /* if a CIK was entered */ cik_info = EDGARLookupCIK(cik); /* get CIK info */ if (ArrayGetAxisDepth(cik_info)==0){ /* if response is empty */ MessageBox('x',INVALID_CIK,cik); /* display error */ return ERROR_EXIT; /* return back to menu */ } /* */ } /* */ if (IsFolder(loc)==false){ /* if it's not a folder */ rc = CreateFolder(loc); /* try to create it. */ if (IsError(rc)){ /* if we cannot */ MessageBox('x',INVALID_FOLDER,loc); /* display error */ return ERROR_EXIT; /* return back to menu */ } } /* */ if (loc != "(default)"){ /* if not a default location */ PutSetting(LINK_INI,"SEC Exhibit Linker", /* set ini output */ "EDGAR Cache Location",loc); } /* */ PutSetting(LINK_INI,"Filter Setting","CIK",cik); /* set ini output */ }
It’s important to validate the information the user provides both to be certain the CIK is real and that the script can access whatever location has been specified for the cache. After defining variables we need, this function gathers the user’s entries and stores the data into the cik and loc variables using the EditGetText function. If the cik variable isn’t empty, we validate it by trying to retrieve information associated with that CIK using the EDGARLookupCIK SDK function. If the array it returns is empty, we can alert the user that the provided CIK is invalid with our predefined error message.
Similarly, we can use the IsFolder function on the loc variable. If the folder does not exist, we can attempt to create it with the CreateFolder function. If that fails (for example, if the script does not permission to write to this location), we can alert the user. These simple checks ensure the information the user provided is valid.
If everything checks out, the PutSetting function will save our new parameters as INI entries. Again, we must specify the section and entry name that are used in the INI file. We store the new CIK and the new cache location before returning without an error.
And that’s it. We’ve taken the Exhibit Linker, a powerful tool already written with Legato, and extended it even further by allowing the user to specify the default CIK to look up and the default location to store the filing information. This can save time, particularly in a production environment where one CIK and therefore one cache is commonly used.
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
Quicksearch
Categories
Calendar
November '24 | ||||||
---|---|---|---|---|---|---|
Mo | Tu | We | Th | Fr | Sa | Su |
Thursday, November 21. 2024 | ||||||
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 |