Showing posts with label Sonraw. Show all posts
Showing posts with label Sonraw. Show all posts

Monday, 16 June 2025

Showing 'info only' parts in BOMs

I spent an hour banging my head against a wall today, trying to find the correct invocation to SONRAW that would result in showing all sons, including those that are marked as 'info only'. The basic form of the command is

EXECUTE SONRAW :$.PRT, SQL.DATE8, :$.ARC, :$.OPT, :$.REV
where: :$.PRT is a linked file of parts that one wants to 'explode'
SQL.DATE8 is of course, today - this may have some function with trees that have revisions
:$.ARC is the result, a linked file of tuples of PARTARC
:$.OPT is the desired option, the raison d'etre of this blog
:$.REV is apparently something to do with revisions ... or not.

If one dissects the various values of :$.OPT as in the PARTTREE procedure, 0 = regular, 2 = including phantoms, 4 = including info only parts. These values can be combined as they form a bit mask. The value of :$.REV had me scratching my head: if :$.OPT is 0 then :$.REV can be empty and one does get some form of tree, but it seems best that :$.REV always be set to '-N'. I wasted a great deal of time trying to find the right invocation to include info-only sons where :$.REV was empty.
  • So the invocation for a 'normal' BOM would be
    EXECUTE SONRAW :$.PRT, SQL.DATE8, :$.ARC, 0, '-N'
  • For a 'normal' BOM with phantoms would be
    EXECUTE SONRAW :$.PRT, SQL.DATE8, :$.ARC, 2, '-N'
  • And for a 'normal' BOM with info-only sons would be
    EXECUTE SONRAW :$.PRT, SQL.DATE8, :$.ARC, 4, '-N'
Cut this out and store it somewhere.

Friday, 16 August 2024

More fixing a complicated problem with BOM revisions and 'info only' parts

The code given in the previous blog was almost correct, but subtly wrong. I had ignored the fact that each son part would appear three times, and that the first instance would set SONACT to be -1 thus causing the son not to appear in the report, even though there would be a correct instance later on. A more correct version of this code is

SUB 820; :OK = 0; /* the query below may fail */ :RVDATE = 01/01/88; SELECT MAX (ORIG.RVTILLDATE) INTO :RVDATE FROM PARTARC ORIG WHERE ORIG.PART = :PARENT AND ORIG.SON = :SON AND ORIG.INFOONLY <> 'Y'; :OK = (:RVDATE = 01/01/50 ? 1 : 0); GOTO 821 WHERE :OK = 1; UPDATE PARTARC /* effectively remove the part from the tree */ SET SONACT = -1 WHERE SON = :SON; LABEL 821; RETURN;

Saving the maximum value of RVTILLDATE into a variable was not in my original code; this was intended to help with running the procedure with the debugger. But for some reason the 'wonderful' web interface didn't work so I didn't get an automatic debug output.

There was a strange phenomenon whose source I failed to track down, even after wasting an hour on it. The part that I was using for testing should have four active sons, but for some reason five were being displayed. That fifth part had been marked 'for info only' in all the versions so the 'select max' query above should have failed. But somehow this fifth part never even got to the subroutine, a failure that I could not track. Eventually I added a cursor that is called immediately after the SONRAW procedure.

DECLARE C840 CURSOR FOR SELECT SON, SONPARTREV FROM PARTARC WHERE SON > 0; OPEN C840; GOTO 843 WHERE :RETVAL <= 0; LABEL 841; FETCH C840 INTO :SON, :PARENT; GOTO 842 WHERE :RETVAL <= 0; :A = :B = 0; SELECT COUNT (*), SUM ((INFOONLY = 'Y' ? 1 : 0)) INTO :A, :B FROM PARTARC PA WHERE PART = :PARENT AND SON = :SON; LOOP 841 WHERE :A <> :B; UPDATE PARTARC /* this is the linked table */ SET SONACT = -1 WHERE SON = :SON; LOOP 841; LABEL 842; CLOSE C840; LABEL 843;

This cursor got rid of the spurious part and probably helped the rest of the procedure run a bit faster as there would have been fewer parts to check in the main cursor.

Working with BOM revisions is very complicated!

Thursday, 15 August 2024

Fixing a complicated problem with BOM revisions and 'info only' parts

For a customer, I wrote a year ago a procedure and report that explodes a part's BOM and calculates various data for the part's sons. I thought that the report was working fine until the customer brought to my attention a specific part: despite the fact that its sons appear as "for info only" (FIO) in the BOM table, these son parts were appearing in the report.

How I reached the solution for the next step is lost to me, so I'll say that by divine intervention I saw that the father part had three BOM revisions, and that the sons that were marked FIO in the BOM table were not marked as such in one of these revisions.

The procedure uses the SONRAW program to explode the BOM, where the fourth parameter is 0, i.e. don't include parts marked as FIO. How is it that these parts still appear? Could it be that SONRAW is working on all the BOM revisions and so is finding the revision in which the parts are not marked as FIO?

If one dumps the data contained in the table PARTARC for the given father part, one will see that the son parts appear three times, once for each revision. The difference between these apparently identical lines, apart from the FIO flag, is that their RVFROMDATE and RVTILLDATE fields differ; these values are identical to the revisions in the BOM revisions table of the father.

Whilst walking the dog after the consultation, I thought about this problem and concluded that first I would have to find the line with the maximum value for RVFROMDATE then check whether this line is marked as FIO. It seems that the right hemisphere of my brain was working on this problem all night because at about 2:30 am, I suddenly realised that I don't have to look for a maximum value - I simply need to find the line with RVTILLDATE = 01/01/50.

This morning I came in fired up with enthusiasm and added this condition to the cursor that iterates over the sons. Nothing changed. It turns out that SONRAW zeroes out this field in the linked PARTARC table: annoying. I tried another track: I know that PARTARC.SONPARTREV contains the internal part number of the direct parent. Via this field I could access the original, unlinked, tuples of PARTARC in order to find the appropriate line. As the check has to occur twice in the procedure, I wrote a subroutine that contains the code that checks. 

The part at the end of the subroutine that sets SONACT to be -1 is because the linked PARTARC table is passed to the report that 'knows' not to display lines where SONACT = -1. This was something that eluded me until late in the day. The variable :PARENT is PARTARC.SONPARTREV. The returned variable :OK will cause the procedure to skip parts of the code if the value is 0.

/************************************************************** SUB 820 - Check whether son is in the current version of the BOM and if so, is info only ***************************************************************/ SUB 820; :OK = 1; SELECT 0 INTO :OK FROM PARTARC ORIG WHERE ORIG.PART = :PARENT AND ORIG.SON = :SON AND (ORIG.INFOONLY = 'Y' OR ORIG.RVTILLDATE <> 01/01/50); GOTO 821 WHERE :OK = 1; UPDATE PARTARC /* effectively remove the part from the tree */ SET SONACT = -1 WHERE PART = :FATHER AND SON = :SON; LABEL 821; RETURN;

Wednesday, 6 July 2022

Programming challenge with BOMs (2)

Somehow I doubt that anyone is going to take up my challenge, so I'll try and document my code here.  First, I'll go through some descriptions of the challenge: every line/part in the BOM has a level; direct sons of the father part have level 1, sons of the sons have level 2, etc. If the BOM consisted solely of direct sons, then their numbers would be 1, 2, 3, etc. When a grandson is encountered, the current level 1 number has to be remembered whilst the grandsons get numbered consecutively (i.e. 3.1, 3.2, 3.3). When there are no more grandsons then the program has to continue incrementing the previous level number.

In simple programming terms, a 'previous level' variable is maintained; this variable is compared to the 'current level' variable. If the two levels are the same then the 'level counter' is incremented; if the current level is greater than the previous level then a new counter should commence, and should the current level be less than the previous level, then the previous counter should be restored.

What took me some time to figure out was how I maintain these different level counters. If I were using a normal imperative programming language then I would use an array to do this, but the Priority flavour of SQL doesn't have arrays. Another possibility would be pushing the current level counter onto a stack, then later popping that value; this could be done by means of recursion or a software stack implementation (which can be emulated by an array), but again, these are tools that I don't have at my disposal.

I'm not sure whether the hint came from that phrase, pushing onto a stack, or maybe it was my right brain hemisphere delivering the goods, but suddenly it became clear that I could use a database table (specifically, STACK2) to implement the array. STACK2 has two fields: ELEMENT (the key) and TYPE, so ELEMENT will hold the level and TYPE the counter for that level. Once I had this key element (sorry!) sorted, the rest of the algorithm fell into place.

While I was still going over the algorithm in my head, I considered how many rows I would need in STACK2: what would happen if I defined a given number of rows then the BOM had one more level? Then I realised that I could first establish the maximum level in the BOM then enter rows appropriately. Entering the rows at the beginning means that from hereon the procedure only has to UPDATE STACK2 - there will be no need to test whether the required row exists thus no need for INSERTs.

LINK STACK2 TO :$.ST2; ERRMSG 1 WHERE :RETVAL <= 0; :MAX = 0; SELECT MAX (VAR) INTO :MAX FROM PARTARC; :PREVIOUS = 0; LABEL 1; :PREVIOUS = :PREVIOUS + 1; INSERT INTO STACK2 (ELEMENT) VALUES (:PREVIOUS); LOOP 1 WHERE :PREVIOUS < :MAX; :PREVIOUS = 0; DECLARE C30A CURSOR FOR SELECT SONACT, VAR FROM PARTARC WHERE SONACT > 0 ORDER BY SONACT; OPEN C30A; GOTO 300 WHERE :RETVAL <= 0; /* ??? */ LABEL 100; FETCH C30A INTO :SONACT, :LEVEL; GOTO 200 WHERE :RETVAL <= 0; GOTO 110 WHERE :PREVIOUS < :LEVEL; GOTO 120 WHERE :PREVIOUS = :LEVEL; GOTO 130 WHERE :PREVIOUS > :LEVEL; LABEL 110; :PREVIOUS = :LEVEL; UPDATE STACK2 SET TYPE = 1 WHERE ELEMENT = :PREVIOUS; GOTO 150; LABEL 120; UPDATE STACK2 SET TYPE = TYPE + 1 WHERE ELEMENT = :PREVIOUS; GOTO 150; LABEL 130; :PREVIOUS = :LEVEL; UPDATE STACK2 SET TYPE = TYPE + 1 WHERE ELEMENT = :PREVIOUS; LABEL 150; :STRING = :STR = ''; SELECT ITOA (TYPE) INTO :STRING FROM STACK2 WHERE ELEMENT = 1; GOTO 160 WHERE :PREVIOUS = 1; :N = 1; LABEL 1; :N = :N + 1; SELECT ITOA (TYPE) INTO :STR FROM STACK2 WHERE ELEMENT = :N; :STRING = STRCAT (:STRING, '.', :STR); LOOP 1 WHERE :N < :PREVIOUS; LABEL 160; UPDATE STACK4 SET DOCNO = :STRING WHERE KEY = :SONACT; LOOP 100; LABEL 200; CLOSE C30A; LABEL 300; UNLINK AND REMOVE STACK2;

The first part of the code (upto 'DECLARE C30A CURSOR') is concerned with setting up STACK2 as I described previously. The cursor then iterates over the exploded BOMs, retrieving the key number (saved in SONACT) and the line level (saved in VAR). As I noted in an earlier blog on the topic, these fields have one set of meanings when PARTARC is 'raw' and another set of meanings when PARTARC holds exploded BOMs, after having run SONRAW. This is somewhat distracting.

The next part of the code - the three comparisons between the previous level and the current level - implements what I described in the second paragraph of this blog. Following this, the code beginning at label 150 is concerned with building the string to be saved; level 1 is made into a special case - this is in order to handle the separating dots more easily. It doesn't matter that there might be values in the later rows of the table - the loop (that ends with LOOP 1 WHERE :N < :PREVIOUS) means that these values won't be accessed. The final statement saves the calculated string in the field STACK4.DOCNO.

I am proud to say that I worked all of this out in my head before I typed it last night, although there was one small glitch that caused the procedure not to work. By mistake, I had defined that the insertions into STACK2 at the beginning were into field TYPE, which is completely wrong. ELEMENT is the key of this table and as no value was inserted, nothing went into STACK2, making the rest of the program fairly useless. Once I corrected this, the procedure worked exactly as it was supposed to.

I really would be interested to see how other people would solve this challenge, as I make no claims that the above code is optimal.

Tuesday, 5 July 2022

Programming challenge with Bills Of Materials

The standard reports for displaying bills of materials (BOMs) that come with Priority show the depth of a part in the BOM in a field that is slightly complicated to program. Instead of trying to explain this in words, I'll use a table

Number Part description Father Level
1 A chair - 0
2 Arm rests 1 1
3 Mechanism 1 1
4 Back 1 1
5 Wood 4 .2
6 Sponge 4 .2
7 Fabric 4 .2
8 Back rest 1 1
9 Wood 8 .2
10 Sponge 8 .2
11 Fabric 8 .2

A chair is built from arm rests, a mechanism (for raising and lowering the seat), a seat and a back rest. For these parts, their father is the chair, or in terms of the above table, part #1. A seat (and a back rest) is made from wood, sponge and fabric; for these parts, their father is the seat (or back rest), part #4 or part #8. With regard to levels, the chair is at level 0; the arm rests, mechanism, seat and back rest are at level 1 (direct sons of the chair); the wood, sponge and fabric are at level 2, which is shown as .2 in the above table.

A customer did not want the level to be displayed as above; instead, he wants that in the field equivalent to level, the arm rests will appear as 1 (no change), the mechanism as 2, the seat as 3, the wood of the seat as 3.1, etc. The table below shows how the customer wants the level to be displayed.

Number Part description Father Level
1 A chair - 0
2 Arm rests 1 1
3 Mechanism 1 2
4 Back 1 3
5 Wood 4 3.1
6 Sponge 4 3.2
7 Fabric 4 3.3
8 Back rest 1 4
9 Wood 8 4.1
10 Sponge 8 4.2
11 Fabric 8 4.3

This might seem to the customer to be a reasonable request, but in terms of Priority (or any SQL based language) this - at least to me - is extremely difficult to program. It took me half an hour of walking the dog to come up with a solution, but I am not convinced that this is the best/only way of creating those values.

So I am opening this problem as a programming challenge: send me code that achieves what the customer requested.

Some assumptions:
The external program SONRAW has been run, and the exploded BOMs are in a linked copy of PARTARC.
There is a linked table STACK4 whose key is PARTARC.SONACT, i.e. an index (see here for a discussion how SONACT can be used in conjunction with STACK4).
The level string should be stored in STACK4.DOCNO.

Sunday, 23 January 2022

Sonraw (2)

Nearly a year ago, I wrote about the external procedure SONRAW and how it changes values in the linked PARTARC table. I've just made an important discovery about this table after having run SONRAW.

Let's say that there is a part P0 that is the 'grandfather'; P1 will be both a son of P0 and a father, and R1 is a purchase part that is a son of P1. Assuming that there are no more sons in the BOMs of P0 and P1, and that SONRAW is 'fed' part P0, after SONRAW is run, there will be two rows in the linked PARTARC table. In both rows, PARTARC.PART will be P0; in the first row, PARTARC.SON will be P1 and in the second row, PARTARC.SON will be R1. So far, nothing new.

What I have just discovered is that in the first row, PARTARC.SONPARTREV will be P0 (not very useful), but in the second row, PARTARC.SONPARTREV will be P1! This means that the direct father of any purchase part can be found in the linked PARTARC table, something that I thought was not possible. This is even better if there is a phantom between P1 and R1, because looking at the unlinked PARTARC of P1 will show the phantom. Also, there's no real way of getting the parent of R1 - until now!

Not surprisingly, this is very important for a report that I am trying to write; the original version obtained the purchase parts, which is fine, but now I need to get some values from the direct father.

[Edit from 03/10/22: After SONRAW, PARTARC.SONQUANT also contains the internal number of the direct father, although again, this is a real number so one has to write PART.PART = ROUND (PARTARC.SONQUANT).]

Wednesday, 14 April 2021

Sonraw

The external program SONRAW is invaluable: it takes a list of parts and builds an 'exploded' bill of materials for each part. Unfortunately, there are also a few very important 'gotchas' of which we have to be aware. These arise due to what might be considered a poor decision by the original system developers: the output of SONRAW is stored in the table PARTARC, which is the same table in which are stored the default BOM data.

A brief explanation 
Putting this another way, the 'normal' table PARTARC stores a parent part and its direct sons, i.e. to a depth of 1. Let's say that there is a parent part A and has sons B, C and D: there will be three rows in the PARTARC table where the parent is A. But B, C and D might have sons of their own (eg B1, B2, B3, C1, etc). In this case there will be three rows in PARTARC where the parent is B (the sons will be B1, B2 and B3). If it's not yet clear, a part can appear both in the 'part' field of PARTARC, when it is a parent, and in the 'son' field, when it is a son.

What does SONRAW do? If there are three rows, A/B, A/C and A/D, and another three rows B/B1, B/B2 and B/B3, the resulting linked PARTARC table will also contain six rows, but these will be A/B, A/B1, A/B2, A/B3, A/C and A/D although not necessarily in this order. There is a field in PARTARC that stores the depth of the son; for A/B, the depth will be 1, but for A/B1, the depth will be 2. This data enables one to write a relatively simple report that displays the recursive tree as a list.

How does one use SONRAW? 
One can use SONRAW as a separate procedure step (see procedure PARTTREE, stage 40) or one can call SONRAW within an SQLI step; it's slightly more than a matter of preference - I find it easier to call SONRAW within an SQLI step. Whichever way, there must be five parameters passed; these are 

  1. a table linked to PART - this will contain the parent parts
  2. a date - I think that this is useful when installations have different versions of BOMs. I don't have this requirement and so always pass today's date
  3. a table linked to PARTARC - this will contain the output
  4. an integer between 0 and 4 - this indicates which option to use (look at PARTTREE, stages 10-30). I normally use 0; 2 will include phantoms in the output (?)
  5. a flag that can either be empty or '-N'; the 'N' probably stands for 'normal'. Leaving the flag empty will cause the output to be partial; see PARTTREE stage 10 for 'an explanation'
My normal code for using this is as follows
LINK PART TO :$.PRT; ERRMSG 1 WHERE :RETVAL <= 0; LINK PARTARC TO :$.ARC; ERRMSG 1 WHERE :RETVAL <= 0; /* the parameter 2 means show phantoms as well as standard parts */ EXECUTE SONRAW :$.PRT, SQL.DATE8, :$.ARC, 2, '-N';
What comes after this depends on the application. One can unlink the tables, then pass PARTARC to a report that prints out the tree. I often have a cursor that runs after the SONRAW code in order to extract only purchase parts (and writing this now, I realise that the fourth parameter to the call above is '2'; according to PARTTREE, this means that only R parts will be included in the output).

So where are the pitfalls?

There are several fields in the PARTARC table that appear (unsurprisingly) in the PARTARC screen (and also PARTARCONE) that SONRAW trashes. Prior to SONRAW, the field PART contains the internal part number of a given parent (e.g. B in the simple example about), SCRAP will contain the percentage scrap of part B1 (the calculation of the scrap is not straightforward, but that's another story),  SONREVNAME contains the internal number of the son revision chosen for this line, and SONQUANT contains the quantity of B1 needed for one unit of B. But after SONRAW, PART contains the internal part number of the top part (i.e. A), COEF will contain the quantity of B1 needed for one unit of A, and SONQUANT will contain the internal part number of the original parent (i.e. B). SCRAP and SONREVNAME contain values but they're not what they were prior to running SONRAW (and I don't know what they are).

PARTARC.SONACT also gets trashed but in this case, the new value after SONRAW is very useful: it holds a sequential index to the lines in PARTARC (i.e. 1, 2, 3). This gets used in any report based on PARTARC as it presents the lines in the correct, hierarchical, order. But this index has added value: it can be used as an index into an auxiliary table (e.g. STACK4) that holds additional data for each line in the exploded tree.

In the past, I discovered that I have to write some obscure code in order to obtain the scrap and revision number, as follows.

DECLARE C3 CURSOR FOR SELECT PARTARC.SON, PARTARC.COEF, PARTARC.VAR, PARTARC.SONACT, ROUND (PARTARC.SONQUANT) FROM PARTARC, PART ORIG WHERE PARTARC.SON = ORIG.PART AND PARTARC.PART = :PART AND PARTARC.SON > 0 ORDER BY 4; OPEN C3; GOTO 300 WHERE :RETVAL <= 0 ; LABEL 210; FETCH C3 INTO :SON, :COEF, :VAR, :SONACT, :SQ; GOTO 299 WHERE :RETVAL <= 0; :SCRAP = 0.0; :REV = 0; SELECT SCRAP, SONREVNAME INTO :SCRAP, :REV FROM PARTARC ORIG WHERE PART = :SQ AND SON = :SON; INSERT INTO STACK4 (KEY, REALDATA, INTDATA) VALUES (:SONACT, :SCRAP, :REV);

The final lines above show :SCRAP and :REV being stored into an auxiliary table that gets passed to the report along with the linked PARTARC table, so that the report can show the necessary values. The cursor uses ROUND (PARTARC.SONQUANT) as SONQUANT is a real number, but an internal part number is an integer, of course.

I wasted about an hour today because of this problem with SONREVNAME. It's not a field that I use myself, but a company for whom I am writing an exceedingly complicated BOM report needs it. Eventually the penny dropped as to the problem: I already had the code for obtaining the correct value of SCRAP so I adopted it for this new program.

Friday, 5 February 2021

A safe method of extracting 'son' parts from a bill of materials

I am often asked to prepare a type of report that requires descending through a part's bill of materials and doing something with the 'son' parts, for example showing the cost of raw materials for each part and the cost of work for that part. Such reports (or more correctly, the procedure that prepares the data to be shown in the report) use the external SONRAW procedure along with temporary PART and PARTARC tables. It also happens that the data for each primary part is to be stored separately; for example, such a report showing raw materials for an order could display the data in two different ways: either all the parts required for the complete order, or all the parts required for each line in the order. In the first case, SONRAW would be called once for the order, whereas the second case might require SONRAW to be called in a loop.

I would start such a procedure with the lines

LINK PART TO :$.PRT; ERRMSG 1 WHERE :RETVAL <= 0; LINK PARTARC TO :$.ARC; ERRMSG 1 WHERE :RETVAL <= 0;
Then parts would be entered into the linked table in the following manner
INSERT INTO PART (PART, PARTNAME, FATQUANT) SELECT ORIG.PART, ORIG.PARTNAME, REALQUANT (SUM (ORDERITEMS.QUANT)) FROM ORDERITEMS, PART ORIG WHERE ORDERITEMS.PART = ORIG.PART AND ORDERITEMS.ORD = :ORD AND ORIG.TYPE = 'P' GROUP BY 1, 2; EXECUTE SONRAW :$.PRT, SQL.DATE8, :$.ARC, 0, '-N';
The above example assumes that the code is run in a loop, each time for a different order (represented by :ORD). After SONRAW, something would be done with the 'exploded tree' that is stored in the linked PARTARC table.

So far, so good. So where is the problem? It would often happen that I would add two lines before the 'INSERT INTO PART' statement -
DELETE FROM PART; DELETE FROM PARTARC;
These are linked tables, right? And so the deletion takes place on these linked tables. And yet somehow, whilst running a procedure that uses this code yesterday, the real PART and PARTARC tables were deleted! I have run such code thousands of times and there has never been a problem with it, but something must have happened to prevent the linkage and so the deletion removed the real tables. 'Fortunately' this was at the beginning of the day, so the previous day's backup was sufficient to restore the missing data.

As this is the second time in a month that I have managed to delete real tables, I am now trying to adopt the attitude that the DELETE statement MUST NEVER BE USED!!! So how can I ensure that I have empty temporary tables before the insertion of new parts? By using UNLINK and REMOVE:
UNLINK AND REMOVE PART; UNLINK AND REMOVE PARTARC; LINK PART TO :$.PRT; ERRMSG 1 WHERE :RETVAL <= 0; LINK PARTARC TO :$.ARC; ERRMSG 1 WHERE :RETVAL <= 0; INSERT INTO PART (PART, PARTNAME, FATQUANT) SELECT ORIG.PART, ORIG.PARTNAME, REALQUANT (SUM (ORDERITEMS.QUANT)) ...
This looks like code waiting to be optimised - surely unlinking and then relinking a table will take time - but this is safe code, something much more important than saving a few seconds (as opposed to shutting down a company for several hours whilst data is restored - I claim that I'm checking the disaster recovery procedure).

What does the SDK have to say on this topic? Use the AND REMOVE option if you wish the linked file to be deleted when it is unlinked. This is necessary when working with loops, particularly when manipulations are carried out on the data in the linked file. If you do not remove the linked file, and the function using LINK and UNLINK is called more than once, you will receive the same copy of the table during the next link. So, if you want the LINK command to open a new (updated) copy of the table, use UNLINK AND REMOVE. That's fairly clear for a change. So as modern day Americans say (in English that would have thrown me out of school), "my bad".