问题
Using Google Sheets, I want to automatically number rows like so:
The key is that I want this to use built-in functions only.
I have an implementation working where child items are in separate columns (e.g. "Foo" is in column B, "Bar" is in column C, and "Baz" is in column D). However, it uses a custom JavaScript function, and the slow way that custom JavaScript functions are evaluated, combined with the dependencies, possibly combined with a slow Internet connection, means that my solution can take over one second per row (!) to calculate.
For reference, here's my custom function (that I want to abandon in favor of native code):
/**
* Calculate the Work Breakdown Structure id for this row.
*
* @param {range} priorIds IDs that precede this one.
* @param {range} names The names for this row.
* @return A WBS string id (e.g. "2.1.5") or an empty string if there are no names.
* @customfunction
*/
function WBS_ID(priorIds,names){
if (Array.isArray(names[0])) names = names[0];
if (!names.join("")) return "";
var lastId,pieces=[];
for (var i=priorIds.length;i-- && !lastId;) lastId=priorIds[i][0];
if (lastId) pieces = (lastId+"").split('.').map(function(s){ return s*1 });
for (var i=0;i<names.length;i++){
if (names[i]){
var s = pieces.concat();
pieces.length=i+1;
pieces[i] = (pieces[i]||0) + 1;
return pieces.join(".");
}
}
}
For example, cell A7 would use the formula:=WBS_ID(A$2:A6,B7:D7)
...to produce the result "1.3.2"
Note that in the above example blank rows are skipped during numbering. An answer that does not honor this—where the ID is calculated determinstically from the ROW())—is acceptable (and possibly even desirable).
Edit: Yes, I've tried to do this myself. I have a solution that uses three extra columns which I chose not to include in the question. I have been writing equations in Excel for at least 25 years (and Google Spreadsheets for 1 year). I have looked through the list of functions for Google Spreadsheets and none of them jumps out to me as making possible something that I didn't think of before.
When the question is a programming problem and the problem is an inability to see how to get from point A to point B, I don't know that it's useful to "show what I've done". I've considered splitting by periods. I've looked for a map equivalent function. I know how to use isblank() and counta().
回答1:
Lol this is hilariously the longest (and very likely the most unnecessarily complicated way to combine formulas) but because I thought it was interesting that it does in fact work, so long as you just add a 1 in the first row then in the second row you add:
=if(row()=1,1,if(and(istext(D2),counta(split(A1,"."))=3),left(A1,4)&n(right(A1,1)+1),if(and(isblank(B2),isblank(C2),isblank(D2)),"",if(and(isblank(B2),isblank(C2),isnumber(indirect(address(row()-1,column())))),indirect(address(row()-1,column()))&"."&if(istext(D2),round(max(indirect(address(1,column())&":"&address(row()-1,column())))+0.1,)),if(and(isblank(B2),istext(C2)),round(max(indirect(address(1,column())&":"&address(row()-1,column())))+0.1,2),if(istext(B2),round(max(indirect(address(1,column())&":"&address(row()-1,column())))+1,),))))))
in my defense ive had a very long day at work - complicating what should be a simple thing seems to be my thing today :)
回答2:
Foreword
Spreadsheet built-in functions doesn't include an equivalent to JavaScript .map. The alternative is to use the spreadsheets array handling features and iteration patterns.
A "complete solution" could include the use of built-in functions to automatically transform the user input into a simple table and returning the Work Breakdown Structure number (WBS) . Some people refer to transforming the user input into a simple table as "normalization" but including this will make this post to be too long for the Stack Overflow format, so it will be focused in presenting a short formula to obtain the WBS.
It's worth to say that using formulas for doing the transformation of large data sets into a simple table as part of the continuous spreadsheet calculations, in this case, of WBS, will make the spreadsheet to slow to refresh.
Short answer
To keep the WBS formula short and simple, first transform the user input into a simple table including task name, id and parent id columns, then use a formula like the following:
=ArrayFormula(
IFERROR(
INDEX($D$2:$D,MATCH($C2,$B$2:$B,0))
&"."
&COUNTIF($C$2:$C2,C2),
RANK($B2,FILTER($B$2:B,LEN($C$2:$C)=0),TRUE)&"")
)
Explanation
First, prepare your data
- Put each task in one row.
Include a General task / project to be used as the parent of all the root level tasks. - Add an ID to each task.
- Add a reference to the ID of the parent task for each task.
Left blank for the General task / project.
After the above steps the data should look like the following:
+---+--------------+----+-----------+ | | A | B | C | +---+--------------+----+-----------+ | 1 | Task | ID | Parent ID | | 2 | General task | 1 | | | 3 | Substast 1 | 2 | 1 | | 4 | Substast 2 | 3 | 1 | | 5 | Subsubtask 1 | 4 | 2 | | 6 | Subsubtask 2 | 5 | 2 | +---+--------------+----+-----------+
Remark: This also could help to reduce of required processing time of a custom funcion.
Second, add the below formula to D2, then fill down as needed,
=ArrayFormula(
IFERROR(
INDEX($D$2:$D,MATCH($C2,$B$2:$B,0))
&"."
&COUNTIF($C$2:$C2,C2),
RANK($B2,FILTER($B$2:B,LEN($C$2:$C)=0),TRUE)&"")
)
The result should look like the following:
+---+--------------+----+-----------+----------+ | | A | B | C | D | +---+--------------+----+-----------+----------+ | 1 | Task | ID | Parent ID | WBS | | 2 | General task | 1 | | 1 | | 3 | Substast 1 | 2 | 1 | 1.1 | | 4 | Substast 2 | 3 | 1 | 1.2 | | 5 | Subsubtask 1 | 4 | 2 | 1.1.1 | | 6 | Subsubtask 2 | 5 | 2 | 1.1.2 | +---+--------------+----+-----------+----------+
回答3:
Here's an answer that does not allow a blank line between items, and requires that you manually type "1" into the first cell (A2). This formula is applied to cell A3, with the assumption that there are at most three levels of hierarchy in columns B, C, and D.
=IF(
COUNTA(B3), // If there is a value in the 1st column
INDEX(SPLIT(A2,"."),1)+1, // find the 1st part of the prior ID, plus 1
IF( // ...otherwise
COUNTA(C3), // If there's a value in the 2nd column
INDEX(SPLIT(A2,"."),1) // find the 1st part of the prior ID
& "." // add a period and
& IFERROR(INDEX(SPLIT(A2,"."),2),0)+1, // add the 2nd part of the prior ID (or 0), plus 1
INDEX(SPLIT(A2,"."),1) // ...otherwise find the 1st part of the prior ID
& "." // add a period and
& IFERROR(INDEX(SPLIT(A2,"."),2),1) // add the 2nd part of the prior ID or 1 and
& "." // add a period and
& IFERROR(INDEX(SPLIT(A2,"."),3)+1,1) // add the 3rd part of the prior ID (or 0), plus 1
)
) & "" // Ensure the result is a string ("1.2", not 1.2)
Without comments:
=IF(COUNTA(B3),INDEX(SPLIT(A2,"."),1)+1,IF(COUNTA(C3),INDEX(SPLIT(A2,"."),1)& "."& IFERROR(INDEX(SPLIT(A2,"."),2),0)+1,INDEX(SPLIT(A2,"."),1)& "."& IFERROR(INDEX(SPLIT(A2,"."),2),1)& "."& IFERROR(INDEX(SPLIT(A2,"."),3)+1,1))) & ""
回答4:
Overview
This an extract of a post in Stack Overflow in Spanish by myself derived from this question. This isn't a translation and there is no intention to keep in sync both posts.
This post include a screenshot, a link to a demo spreadsheet the formulas very few comments.
Columns E to L include array formulas that automatically calculate the required values as more rows are added.
User input columns
User input is expected in columns A to D. If more rows are required add them below the first row, from row 5 or below in the demo file / screenshot).
Don't add non-required blank rows as they will make that the spreadsheet recalculation time be unnecessarily increased.
Formulas of columns E to L
- Columns E to K are auxiliary columns. These columns could be hidden. The column L display the final result. It could be moved.
- Column M has a formula based in the answer by Phrogz with slight changes for validation purposes. This column should be manually filled down from the second row, as the first row includes a constant. All other formulas columns will automatically filled out based on user input (columns A to D)
ID (column E)
=ArrayFormula(ARRAY_CONSTRAIN(ROW(B4:B)-ROW(B3),MAX(IF(LEN(A4:D),ROW(A4:D),0))-1,1))
ID Padre (column F)
=ArrayFormula(
IFERROR(
hlookup(
MMULT(NOT(ISBLANK(A4:D))*{1,2,3,4},TRANSPOSE(SIGN(COLUMN(A4:D))))-1,
{1,2,3,4;{
vlookup(ROW(A4:A)-ROW(B3),{IF(LEN(A4:A)>0,ROW(A4:A)-ROW(B3),""),ROW(A4:A)-ROW(B3)},2),
vlookup(ROW(A4:A)-ROW(B3),{IF(LEN(B4:B)>0,ROW(A4:A)-ROW(B3),""),ROW(A4:A)-ROW(B3)},2),
vlookup(ROW(A4:A)-ROW(B3),{IF(LEN(C4:C)>0,ROW(A4:A)-ROW(B3),""),ROW(A4:A)-ROW(B3)},2),
vlookup(ROW(A4:A)-ROW(B3),{IF(LEN(D4:D)>0,ROW(A4:A)-ROW(B3),""),ROW(A4:A)-ROW(B3)},2)
}}
,ROW(A4:A)-ROW(B3),1),""))
Numeración por nivel jerárquico (columns G to J)
Primer Nivel (column G)
=ArrayFormula(IFERROR(
ARRAY_CONSTRAIN(
vlookup(ROW(A4:A)-1,{IF(LEN(A4:A)>0,ROW(A4:A)-1,""),
ARRAY_CONSTRAIN(
MMULT((F4:F=TRANSPOSE(F4:F))*(COUNT(E4:E)-E4:E<TRANSPOSE(COUNT(E4:E)-E4:E)),
SIGN(ROW(E4:E)))+1,COUNTA(E4:E),1)},2),
MAX(IF(ISBLANK(A4:D),0,ROW(A4:D))),1),""))
Segundo a cuarto nivel (Columns H to J)
Se requieren tres columnas con prácticamente la misma fórmula. Sólo cambia la columnas a las que se hace referencia.
=ArrayFormula(IFERROR(
ARRAY_CONSTRAIN(
vlookup(ROW(B4:B)-1,{IF(LEN(B4:B)>0,ROW(B4:B)-1,""),
ARRAY_CONSTRAIN(
MMULT((F4:F=TRANSPOSE(F4:F))*(COUNT(E4:E)-E4:E<TRANSPOSE(COUNT(E4:E)-E4:E)),
SIGN(ROW(E4:E)))+1
,COUNTA(E4:E),1),1/SIGN(LEN(A4:A))},2),
MAX(IF(ISBLANK($A$4:D),0,ROW($A$4:$D))),
1),""))
Nivel (Column K)
=ArrayFormula(ARRAY_CONSTRAIN(
MMULT(NOT(ISBLANK(A4:D))*{1,2,3,4},TRANSPOSE(SIGN(COLUMN(A4:D)))),
MAX(IF(LEN(A4:D),ROW(A4:D),0))-1,1))
Resultado (Column L)
=ArrayFormula(G4:G&IF(K4:K>=2,"."&H4:H,"")&IF(K4:K>=3,"."&I4:I,"")&IF(K4:K>=4,"."&J4:J,""))
Verication
Cálculo alterno (Column M)
=
IF(COUNTA(A5),
INDEX(SPLIT(M4,"."),1)+1,
IF(COUNTA(B5),
INDEX(SPLIT(M4,"."),1)&"."&
IFERROR(INDEX(SPLIT(M4,"."),2),0)+1,
IF(COUNTA(C5),
INDEX(SPLIT(M4,"."),1)& "."&
IFERROR(INDEX(SPLIT(M4,"."),2),1)& "."&
IFERROR(INDEX(SPLIT(M4,"."),3)+1,1),
INDEX(SPLIT(M4,"."),1)& "."&
IFERROR(INDEX(SPLIT(M4,"."),2),1)& "."&
IFERROR(INDEX(SPLIT(M4,"."),3),1)& "."&
IFERROR(INDEX(SPLIT(M4,"."),4)+1,1)
)
)
&"")
Validación (Column N)
=ArrayFormula(ARRAY_CONSTRAIN(L4:L=M4:M,MAX(IF(LEN(A4:D),ROW(A4:D),0))-1,1))
来源:https://stackoverflow.com/questions/35711081/calculate-hierarchical-labels-for-google-sheets-using-native-functions