Hi. This was a few years ago, but we could not find any way to do this in the standard Ensemble set up at the time. The message viewer can be asked the right sort of questions, but then times out as the query takes too long.

For monthly stats we wrote embedded SQL in a class method that we ran at the command prompt. This wrote out the results as CSV to the terminal and once it finished we copied and pasted the various blocks into Excel. Very slow and manual, but only once a month so not so bad. (Our system only purged messages after many months.)

Then we wanted daily numbers to feed into another spreadsheet. So we built an Ensemble service running once a day that ran similar embedded SQL but only for the one previous day of messages. It put the CSV results into a stream in a custom message that went to an operation, that sent then emails out to a user-defined list. Essential bits below. Hope it helps.

    Set today = $ZDATE($H,3)_" 00:00:00"
    Set yesterday = $SYSTEM.SQL.DATEADD("day",-1,today)
    Set tRequest = ##class(xxx.Mess.Email).%New()
    Set tRequest.To = ..SendTo
...etc
    &sql(DECLARE C1 CURSOR FOR
    SELECT
          SUBSTRING(DATEPART('sts',TimeCreated) FROM 1 FOR 10),
          SourceBusinessType,
          SourceConfigName,
          TargetBusinessType,
          TargetConfigName,
          COUNT(*)
        INTO :mDate,...
        FROM Ens.MessageHeader
        WHERE TimeCreated >= :yesterday AND TimeCreated < :today
        GROUP BY...
        ORDER BY...
    )
    &sql(OPEN C1)
    &sql(FETCH C1)
    While (SQLCODE = 0) {
        Set tSC = tRequest.Attachment.WriteLine(mDate_","_...
        &sql(FETCH C1)
    }
    &sql(CLOSE C1)
    Set tSC = ..SendRequestAsync($ZStrip(..TargetConfigName,"<>W"),tRequest) If $$$ISERR(tSC)...

Hi. The team I work in has been supporting old MUMPS and then Cache systems for many years, and have two rules for fixing data on live systems:

1. Never write a kill statement directly at the command line.

It's quick and convenient, but we've seen many occasions  where big chunks of, or, before the database parameter to stop it, whole globals were removed by accident. Cue major panics, hours of restore and de-journal. etc.

2. Don't write SQL updates directly (SMP, command line, etc).

Because if you get the WHERE wrong then all rows in a table suddenly vanish or have the same value. :-)

So we use a local command line tool for global amendments that uses the old %G style selection search to display one node at a time, so that you can then edit the data, or kill (with a warning first if there are sub nodes you may have forgotten about).

We also have a local command line tool for SQL/objects. This takes in a table name and WHERE clause, and only allows editing and deleting when the query finds only one row.

Anything more than a few nodes/rows and we usually write a proper bit of script on our dev server, and get it checked and tested before running in live. Better safe than sorry.

Hi. If you mean the "For" loop, then it's replaced by recursion - the method calls itself. Basically, it translates only the first character of the string and then appends the rest of the string as converted by the same method. Which then does the same, and so on until it has run out of characters, and it then unwinds the stack putting it all together. Only good for small strings of course, as it could run out of stack space, a common problem with recursion. (That and getting your head around what it's doing!)

I'm not going to win, but I thought I'd try a slightly different approach. With thanks to others for the clever $P part.

 ClassMethod N(As %String, As %String = "") As %String
{
$S(i="":"",1:$S(".,?!"[$E(i):p_$E(i),i?1A.E:p_$ZCVT($E(i),"U")_$P("lfa7ravo7harlie7elta7cho7oxtrot7olf7otel7ndia7uliett7ilo7ima7ike7ovember7scar7apa7uebec7omeo7ierra7ango7niform7ictor7hiskey7ray7ankee7ulu",7,$A($E(i))#32),1:"")_..N($E(i,2,*)," "))

Size: 252

I claim the least number of lines - 1 - and least commands - 1 - and probably the least variables - 2.

Could do without the second input param if you allow a leading space in the output (not mentioned in requirements, but fails for the example output, etc.). I also noticed that the sizing does not include the ClassMethod... line, so you could reduce it by moving strings to default values for input variables, but I think that's much like the other comments that involved moving the implementation outside the method completely, just cheating the counting mechanism rather than solving the problem. :-)

It's Friday afternoon, so I thought I'd play with a variation that avoids $Query. This is for no particular reason, as I think the $Q solution is fine, but I wondered if I could:

ClassMethod Flatten(ByRef array, ref = "array") As %List [ PublicList = array ]
{
                S sub="",list=""
                F {
                                S sub=$O(@ref@(sub),1,data)
                                I sub="" Q
                                I $G(data)'="" {
                                                S line=""
                                                F i=1:1:$QLength(ref) S line=line_$LB($QSubscript(ref,i))
                                                S list=list_$LB($LB(data)_line_$LB(sub))
                                }
                                I $D(@ref@(sub))\10 { ; if there are deeper nodes, go down
                                                S nextRef=$S(ref["(":$E(ref,1,*-1)_",""",1:ref_"(""")_sub_""")"
                                                S list=list_..Flatten(.array,nextRef)
                                }
                }
                Return list
}

It's recursive, which is dangerous but powerful stuff! :-)  Call it by something like:  out=..Flatten(.in)

It also avoids output variables in the parameter list, which I find confusing and against Uncle Bob's clean coding principals (okay, he'd also be horrified by "S" for "set", but I'm old-school). Instead, it returns a list of lists which is just as easy to step through. / Mike

Hi. I've never come across any automatic conversion from character user interface (CHUI) to any web-based one. You might stand a chance if the screens were written in some sort of "screen generator" framework, but most I've seen are basically hand-coded, and the problem is that they work on a field-by-field basis. So they read one field, validate input, update bits of the screen and data, and then move to the next field. This does not match well with web pages that tend to work a screen at a time, so automatic conversion is impossible. Sorry about that. I'd like to be proved wrong, of course - maybe some of the systems I work on could be converted! Anyone?

The reason that there is no sort "function", is that sorting is part of the basic storage structure that the whole system is built on. Arrays, both global and local, are automatically sorted as you set them up (effectively by an insertion sort). No need for a separate program or request. Just set up the data in an array as you go along and it will be sorted when you need it. This has always been a basic idea in the language, right from the original MUMPS, so the documentation may well skip over it a bit. However, when you get used to it, it works well.

Hi,

I recently needed a temporary class (not just a table) to store data while it was manipulated (imported, queried, modified, etc. and eventually exported) . I eventually set up all the storage locations with PPG refs as noted above, which works, but did wonder if there was a class parameter I had missed that just indicated the extent was temporary. Or maybe an alternative to extending %Persistent?

Mike

Hi Stuart,

As others have said, it's best practice to build a new variable, rather than amending what you have (there's some fancy name for the rule, I think, or maybe just "functional programming").

Looking at the string, I assume it is HealthShare HL7 message format, so the contents are pretty limited. In which case maybe a shortcut could be used:

s out=$Replace($Replace(list,"~]",""),"~[","")

Here's another alternative:

w $ZSTRIP($ZSTRIP(list,"*","[]"),"=>P")

I admit it could go horribly wrong with multiple nesting (I don't remember all the possible formats), so needs some testing. 

Hope you're keeping well,

Mike

Sadly, in my team we've all been writing MUMPS for so long that the abbreviated style comes naturally and is a hard habit to break. Yes, expanded is better for new starters in the language.

However... Playing devil's advocate you could say that abbreviated commands are:

1. Faster to type (as you said).

2. More compact. Allowing the reader to "see" more of the structure in one go.

You see, you can expand things out too much, in my opinion. Also, it only takes a few minutes for a (reasonable) programmer to get that "S" means "set", "I" means "if", etc. Commands always appear in the same part of the code (unlike some languages), there are not that many to learn, and once you know them, you can read them! So why bother with extra letters? After all "set" is itself only a token for "put the value on the right of the = into the variable on the left" or something like that. It could be "make" or "update" or "<-" (look up the programming language "APL" on Wikipedia if you want a real scare).

I think the main problem with "old fashioned" code is usually poor label/variable names, squeezing too much on one line, and lack of indenting. It's hard to read mainly because of the other parts of the code, not because the commands are single characters. Some things should be longer to better convey what they are for (though not as long as COBOL), and more lines can help convey program structure.

While I'm here, I'm not that keen on spurious spaces in "set x = 1", as opposed to "set x=1". It just spreads out the important stuff  - spaces are there to split out the commands.  :-) 

Mike

Hi. It depends on what you mean by "certain criteria". If it's a special file name then you could amend the FileSpec  property to skip the ones you don't want yet. If it's in the content, then maybe you should be reading in the file (creating a copy or allowing archive so the original continues to exist) and sending it as a message into Ensemble that can then  be held up in a business process until ready to send out to an Operation that creates an output file. That is the way ensemble is supposed to work, so you get a full record of what happened, etc.

(Otherwise, I'm pretty certain that there are actions that reset the list of processed files - maybe resetting that file path or restarting the job - but I cannot find the documentation about it at the moment. )

Hi.

Recently, well yesterday, I needed to do exactly the same , and on a class property as well! I found an answer more by accident than design:

s data=$LB(1,2,3)

s data=$LI(data,1,*-1)

zw data
data=$lb(1,2)

When I saw the other answer, I worried that this might not work when the result is only one item, but it does, as confirmed by the documentation for the $LIST function. If you supply all three parameters - list, position, end - then it always returns another list. I was pleasantly surprised!

Mike

Hi / It sounds like a good idea! I can think of a number of interfaces I've seen where the target application - a small local system - struggled to keep up with the flow of updates from a large PAS.

The only thing I've done like it was complicated, and had to use a proper Business Process. In that case the "department" was neonatal, so we were only interested in patients admitted to a particular ward. The solution looked for HL7 admissions and transfers to that ward, and when found used the data to create a local record in Caché. Then all other types of message could be checked against those records to see if it needed further processing and passing on (to "BadgerNet" eventually when a full episode was built up). Of course this only works if you can define a clear "starting point" that can be spotted in the message stream.                        / Mike

Hi. The "clean code" people would recommend just one parameter max, and better would be none! But I think that's going a bit far, and agree that 3, or maybe 4, maximum should be aimed for to keep things easy to understand when reading, though there may be exceptions.

The array passing is a good idea to reduce the number for normal routines, but I think the ideal for classes is using objects. If you are truly embracing objects, and self-documenting code, then new classes are usually needed and what used to be parameters become the setting of properties, like this:

table=##class(CMT.UI.Table).%New()
table.TopLine=10,table.BottomLine=21
table.HeaderFormat="Underline"
table.DefineQuery("CMT.UI.PatchSite:MyList")
table.AddColumn(3,,"BOLD")
...etc.

table.Display()

It works well in some cases, but I have to admit that there is a tendency for the number of classes to get a bit silly if you take it to the extreme. Sometimes simple code is best.  :-)     / Mike

I think that is my preferred method as well, but it depends to some extent what you are going to do with the result, and what you want to happen if the input number is too big. This $J solution will always return all the characters input, which may be the safest thing. (Though any space characters inside the input will get converted to zeros.)

When the fail mode needs to still return the same length string, e.g. to avoid messing up some fixed length message format, it might be best to use $E(), e.g.

W $E("0000"_number,$L(a)+1,*)

I've also seen the following used, but I'm not sure I recommend it. So many interesting ways it could go wrong!

w $E(number+10000,2,999)

Hi. I was actually looking up some information about pattern matching, but came across this warning:

If a call attempts to use indirection to get or set the value of object properties, it may result in an error. Do not use calls of this kind, as they attempt to bypass property accessor methods (<PropertyName>Get and <PropertyName>Set). Instead, use the $CLASSMETHOD, $METHOD, and $PROPERTY) functions, which are designed for this purpose

This was from https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GCOS_operators#GCOS_operators_pattern

So it looks like it may well work now, but there's no guarantee it will always work.

Surely there is a $method() call you can use to get the next item in the array?