Multiple AND statements in a FileMaker calculation - filemaker

I want to calculate if a value falls outside of 10% of the last two values added to a database. This calculation is not giving me correct feedback when I have 'Weight" values close to 10, or from 100-110. Otherwise it works fine.
Case (
Get(RecordNumber) ≤ 2 ; "Continue Collecting Data" ;
(((GetNthRecord ( Weight ; Get(RecordNumber)-2))*.9) ≤ Weight) and
(((GetNthRecord ( Weight ; Get(RecordNumber)-2))*1.1) ≥ Weight) and
(((GetNthRecord ( Weight ; Get(RecordNumber)-1))*.9) ≤ Weight) and
(((GetNthRecord ( Weight ; Get(RecordNumber)-1))*1.1) ≥ Weight);
"Stable";
"Unstable")

I’m going to start with the assumption that your table includes fields for both the primary key and the creation timestamp. If not, I would highly recommend adding both to this table and every other table you create.
Assuming these fields are in place, you need to create another occurrence of the table on which this layout is based, then relate the primary key to itself via a Cartesian (×) join. Sort the relationship by creation timestamp descending. Your calculation is then:
Case (
(((GetNthRecord ( Weight ; 1 ) *.9 ) ≤ Weight) and
(((GetNthRecord ( Weight ; 1 ) *1.1 ) ≥ Weight) and
(((GetNthRecord ( Weight ; 2 ) *.9 ) ≤ Weight) and
(((GetNthRecord ( Weight ; 2 ) *1.1 ) ≥ Weight);
"Stable";
"Unstable")
Another thing I noticed is that your code is kind of complex. The Let function can make things easier to read, and your four criteria can be cut down to whether either difference is out of range. So, a simpler version becomes:
Let ( [
#weight1 = GetNthRecord ( all::weight ; 1 ) ;
#weight2 = GetNthRecord ( all::weight ; 2 )
] ; //end define Let
Case (
Abs ( #weight1 - weight ) > .1 ; "Unstable" ;
Abs ( #weight2 - weight ) > .1 ; "Unstable" ;
"Stable"
) //end Case
) //end Let
Does that help?

Assuming you are using FileMaker v12 or later, this looks like a good place to use the ExecuteSQL function (not the script step) to retrieve the last two values. You could do something like this:
Let (
sqlQuery = "
SELECT t.weight
FROM MyTable t
WHERE t.id <> ?
ORDER BY t.id DESC
FETCH FIRST 2 ROWS ONLY
" ;
ExecuteSQL ( sqlQuery ; "" ; "" ; MyTable::id )
)
This query assumes you have a unique 'id' field (i.e. a primary key) that's defined as an 'auto-enter serial' value. The WHERE clause makes sure that the current record (presumably the one the user is entering) is not included in the query. The ORDER BY DESC clause forces the last two records to the top where we can fetch the 'weight' values easily into a value list.
Assuming you use a 'Set Variable' script step to put the query results into $lastValues, you can then test for whether they are in range like so:
Let ( [
lastValue1 = GetValue ( $lastValues ; 1 ) ;
lastValue2 = GetValue ( $lastValues ; 2 ) ;
Value1_InRange = lastValue1 - Abs ( lastValue1 - weight ) >= ( 0.9 * lastValue1 ) ;
Value2_InRange = lastValue2 - Abs ( lastValue2 - weight ) >= ( 0.9 * lastValue2 )
] ;
Value1_InRange and Value2_InRange // returns 1 if both values within range, 0 if not
)
If I were doing this, I would put the above range-checking code into a custom function so it's generic and can be easily reused:
IsWithinRange ( valueToTest ; lastValue ; range ) =
lastValue - Abs ( lastValue - valueToTest ) >= ( ( 1 - range ) * lastValue )
Then the above range-checking code can be reduced to:
IsWithinRange ( MyTable::weight ; GetValue ( $lastValues ; 1 ) ; 0.1 ) &
IsWithinRange ( MyTable::weight ; GetValue ( $lastValues ; 2 ) ; 0.1 )
And one last note.. if you use the ExecuteSQL function in a calculated field, be sure to make it 'unstored' so that it only executes when needed. However, I would recommend you avoid that altogether and simply call it from a script step like 'Set Variable'. That way you can control exactly when it executes.
Hope that helps!

Related

How to calculate using the same measure when related date has no data (past and future)?

After trying a lot of approaches and formulas I decided to ask this question.
See this Matrix visual:
WeekDate is column in a table called Planning. It's related to another date time column Week_Imported in another table called Export.
Export table only has values for the dates: 23-Dec-19, 30-Dec-19 and 06-Jan-20 whereas Planning table has dates spanning multiple weeks in the past and future.
Cumulative Plan Count is calculating correctly as long as there are matching dates between the tables Planning and Export.
Now I'd like to keep calculating even when there's no date matching. I want to get the value 32 from the FIRSTDATE which has data (in this case 23-Dec-2019) and backfill the past with 32.
For dates in the future I'd like to use LASTDATE (06-Jan-20) value which is 89.
Something like this:
WeekDate Cumulative Plan Count
.
.
.
25-Nov-19 32
02-Dec-19 32
09-Dec-19 32
16-Dec-19 32
23-Dec-19 32 <= First WeekDate which has data [backfill past with 32]
30-Dec-19 57
06-Jan-19 89 <= Last WeekDate which has data [fill future with 89]
13-Jan-20 89
20-Jan-20 89
27-Jan-20 89
.
.
.
The formula used for the cumulative SUM is this:
Cumulative Plan Count =
CALCULATE (
ROUNDUP([1 - Target] * MAX(Planning[1 - Plan]), 0)
,
FILTER (
ALL ( Planning[WeekDate] ),
Planning[WeekDate] <= MAX(Planning[WeekDate])
)
)
####### Edit 1 #######
Using this measure below I get 1s for the past...
1 - Target =
VAR minWeek = MIN(Export[Week_Imported])
VAR targetCount =
CALCULATE (
COUNT( 'Export'[1 - Plan]),
FILTER('Export', OR(Export[1 - Plan]="YES", Export[1 - Plan]="_")))
var minTarget = CALCULATE (
COUNT( 'Export'[1 - Plan]),
FILTER('Export', OR(Export[1 - Plan]="YES", Export[1 - Plan]="_")
&& Export[Week_Imported] = minWeek))
RETURN
SWITCH(TRUE,
targetCount = BLANK(), 1, // Here is where I need to get the 1st row value (32) and fill up the column up above...
targetCount)
The problem is that no matter what I do I can't get the 1st value for 23-Dec-2019 (32) to fill up the Cumulative Plan Count column.
This is the result when I use the formula above:
WeekDate Cumulative Plan Count
.
.
.
25-Nov-19 1
02-Dec-19 1
09-Dec-19 1
16-Dec-19 1
23-Dec-19 32 <= First WeekDate which has data
30-Dec-19 57
06-Jan-19 89 <= Last WeekDate which has data
13-Jan-20 89
20-Jan-20 89
27-Jan-20 89
.
.
.
####### Edit 2 #######
I put together a simplified Sample.pbix which shows what I'm trying to
accomplish with minimum data to test things:
https://drive.google.com/drive/folders/1zxS_2VE9_0JEMXvsg9Dq196BK552RbNo?usp=sharing
This screenshot has more details:
https://drive.google.com/open?id=1_-IMEpLwuWWN6vrrT_TNWbeqZ7f1LOan
Let me introduce the solution with intermediate steps.
In your data schema, Planning and Export tables are in one to many relationship. Planning is in the grain of every week, while Export has more rows for each week.
Relationship diagram
On this basis, the measure to count the number of Export rows for each Planning week is as simple as this.
Plan Count (Basic) = COUNTROWS ( 'Export' )
When you slice by Planning[WeekDate], this measure returns the counts of Export rows for the corresponding weeks.
Actually, you need FILTER to count only rows you are interested in.
Plan Count =
COUNTROWS (
FILTER (
'Export',
OR ( 'Export'[Plan] = "YES", 'Export'[Plan] = "_" )
)
)
Here is the result we get so far.
Having this measure as the starting point, we need to extend the calculation to the periods where the data does not exist. In order to do that, we need to handle the filter context where [Plan Count] is evaluated.
We need to get the first and final weeks where Export data exists. Here is a formula that returns the first date of the data regardless of the slicer.
First Data Week =
CALCULATE (
MIN ( Planning[WeekDate] ),
REMOVEFILTERS ( Planning[WeekDate] ),
TREATAS (
CALCULATETABLE (
VALUES ( 'Export'[Week_Imported] ), -- Foreign key referencing Planning[WeekDate]
REMOVEFILTERS ( Planning )
),
Planning[WeekDate]
)
)
We can use this date to modify the filter context to calculate [Plan Count] of the first data week. In fact, below measure always returns 1064, which is the number of [Plan Count] in December 30, 2019.
First Week Plan Count =
VAR _FirstDataWeek = CALCULATE (
MIN ( Planning[WeekDate] ),
REMOVEFILTERS ( Planning[WeekDate] ),
TREATAS (
CALCULATETABLE (
VALUES ( 'Export'[Week_Imported] ),
REMOVEFILTERS ( Planning )
),
Planning[WeekDate]
)
)
RETURN
CALCULATE (
[Plan Count],
Planning[WeekDate] = _FirstDataWeek
)
Using this technique, we can expand the first and final values of [Plan Count] to the past and the future dates. Below is the final formula, which iterates over Planning table, and apply different filter context to calculate [Plan Count].
Extended Plan Count =
-- Planning[WeekDate] values where Export data exists
VAR _DataWeeks = CALCULATETABLE (
VALUES ( Planning[WeekDate] ),
REMOVEFILTERS ( Planning[WeekDate] ),
TREATAS (
CALCULATETABLE (
VALUES ( 'Export'[Week_Imported] ), -- Foreign key referencing Planning[WeekDate]
REMOVEFILTERS ( Planning )
),
Planning[WeekDate]
)
)
-- First and last Planning[WeekDate] where Export data exists
VAR _FirstDataWeek = MINX ( _DataWeeks, [WeekDate] )
VAR _FinalDataWeek = MAXX ( _DataWeeks, [WeekDate] )
-- [Plan Count] values of first and last weeks
VAR _FirstDataWeekPlanCount = CALCULATE ( [Plan Count], Planning[WeekDate] = _FirstDataWeek )
VAR _FinalDataWeekPlanCount = CALCULATE ( [Plan Count], Planning[WeekDate] = _FinalDataWeek )
RETURN
SUMX (
Planning,
SWITCH (
TRUE,
Planning[WeekDate] < _FirstDataWeek, _FirstDataWeekPlanCount,
Planning[WeekDate] > _FinalDataWeek, _FinalDataWeekPlanCount,
[Plan Count]
)
)

replace elements in a 2d array

Given a 2d array
select (ARRAY[[1,2,3], [4,0,0], [7,8,9]]);
{{1,2,3},{4,0,0},{7,8,9}}
Is there a way to replace the slice at [2:2][2:] (the {{0,0}}) with values 5 and 6? array_replace replaces a specific value so I'm not sure how to approach this.
I believe it's more readable to code a function in plpgsql. However, a pure SQL solution also exists:
select (
select array_agg(inner_array order by outer_index)
from (
select outer_index,
array_agg(
case
when outer_index = 2 and inner_index = 2 then 5
when outer_index = 2 and inner_index = 3 then 6
else item
end
order by inner_index
) inner_array
from (
select item,
1 + (n - 1) % array_length(a, 1) inner_index,
1 + (n - 1) / array_length(a, 2) outer_index
from
unnest(a) with ordinality x (item, n)
) _
group by outer_index
)_
)
from (
select (ARRAY[[1,2,3], [4,0,0], [7,8,9]]) a
)_;

getting a calculated result from mixing records in the same table

I am a particular situation on my hands that I do not what to call it, so I am just going to explain it! I am working on File-maker pro database for a university that keeps track of students that are majoring in education one of the tables keeps track of all of the standardized test taken by students that range from the following; ACT, SAT, ETS, and Papa. before when I made this table, all test stood alone or in other words you couldn't combine scores to make a passing grade. That has now changed with two test, PAPA and ETS. In order words, you can combine scores from each test to make one grade to pass.
Here is an example
lets just say for simplicity sake that you need a grand total of 10 for the PAPA and ETS test in order to pass those individual test
Test Name | Reading | Writing | Math | Total | pass/fail
--------------------------------------------------------
PAPA | 2 | 2 | 2 | 6 | FAILED
ets | 1 | 2 | 6 | 9 | FAILED
So A couple weeks ago.. this individual would not pass there standard test, but now this indivdual would hve passed because he/she has gotten a a 6 in the ETS Math and two 2's in Reading and writing in the PAPAS.
Here's what my current calculation looks like:
If (
ReadingScore ≥ BasicTest::PassReading
and MathScore ≥ BasicTest::PassMath
and WritingScore ≥ BasicTest::PassWriting ;
"PASSED" ;
If ( Score >= BasicTest::QMScore ;
If ( ReadingScore >= BasicTest::QMReading ;
If ( MathScore >= BasicTest::QMMATH ;
If ( WritingScore >= BasicTest::QMWriting ;
"PASSED" ;
"DOES NOT MEET WRITING REQUIREMENTS"
) ;
"DOES NOT MEET MATH REQUIREMENTS"
) ;
"DOES NOT MEET READING REQUIREMENTS"
) ;
"FAILED"
)
)
So my question here today is how should I approach this with the Filemaker pro application? Should I make this a report or should a just rebuild the table completely?!?!?!?
While I'm not sure exactly how your calc matches up with your question (I think Score in your calc is the total of the other scores?), I think I can offer some pointers.
Given what you say and what the calc shows, I think the following are true:
Given multiple tests, you're interested in the maximum score for each category.
Each maximum score needs to be greater than the minimum score from either BasicTest::Pass* or BasicTest::QM*.
The test taker passes if all their max scores are greater than all the test minimum scores or the total max scores is greater than the BasicTest::QMScore.
Given the above, I came up with the following calc:
Let (
[
_reading_score = Max ( papa_reading_score ; ets_reading_score ) ;
_math_score = Max ( papa_math_score ; ets_math_score ) ;
_writing_score = Max ( papa_writing_score ; ets_writing_score ) ;
_total_score = _reading_score + _math_score + _writing_score ;
_target_reading = Min ( BasicTest::PassReading ; BasicTest::QMReading ) ;
_target_math = Min ( BasicTest::PassMath ; BasicTest::QMMath ) ;
_target_writing = Min ( BasicTest::PassWriting ; BasicTest::QMWriting ) ;
_pass_reading = ( _reading_score ≥ _target_reading ) ;
_pass_math = ( _math_score ≥ _target_math ) ;
_pass_writing = ( _writing_score ≥ _target_writing ) ;
_passed = ( _pass_reading and _pass_math and _pass_writing )
or ( _total_score ≥ BasicTest::QMScore );
_result = Case (
_passed ; "PASSED" ;
not _pass_reading ; "DOES NOT MEET READING REQUIREMENTS" ;
not _pass_math ; "DOES NOT MEET MATH REQUIREMENTS" ;
not _pass_writing ; "DOES NOT MEET WRITING REQUIREMENTS" ;
"FAILED"
)
] ;
_result
)
Even if the above isn't exactly what you need, I hope it'll provide some direction. With a complicated calc like this, break it up into pieces with each piece (in this case, each variable assignment) being easy to understand.
Note that Max and Min can take many arguments and can also take fields that have multiple values (related fields in a one-to-many relationship or repeating fields), so if you have more than two scores or two tests to check for maximum and minimum values, just add them as more parameters.

Generate series for various mixes of numbers and letteres

I am using this syntax:
generate_series(1, COALESCE((string_to_array(table.id_number, '-')) [2] :: INT, 1)) AS n (numbers)
To generate IDs in elements that have got ID like 32.22.1-4 to get 4 rows with IDs 32.22.1, 32.22.2, 32.22.3 and 32.22.4. How to change it to accept also letters?
So for 32.22.a-c there would be:
32.22.a, 32.22.b, 32.22.c
And for 32.22.d1-d4 there would be
32.22.d1, 32.22.d2, 32.22.d3, 32.22.d4
EDIT:
The whole code looks like:
INSERT INTO ...
(
SELECT
...
FROM table
CROSS JOIN LATERAL
generate_series(1, COALESCE((string_to_array(table.id_number, '-')) [2] :: INT, 1)) AS n (numbers)
WHERE table.id_number LIKE ...
);
WITH t(id_number) AS ( VALUES
('32.33.a1-a5'::TEXT),
('32.34.a-c'::TEXT),
('32.35.b-e'::TEXT)
), stats AS (
SELECT
chars,
chars[1] pattern, -- pattern use
CASE
WHEN (ascii(chars[3]) - ascii(chars[2])) = 0
THEN FALSE
ELSE TRUE
END char_pattern, -- check if series of chars
CASE
WHEN (ascii(chars[3]) - ascii(chars [2])) = 0
THEN right(chars[3],1)::INTEGER
ELSE (ascii(chars[3]) + 1 - ascii(chars[2]))::INTEGER
END i -- number of series
FROM t,
regexp_matches(t.id_number, '(.*\.)(\w*)-(\w*)$') chars
)
SELECT
CASE WHEN char_pattern
THEN pattern || chr(ascii(chars[2]) - 1 + s)
ELSE pattern || left(chars[2],1) || s::TEXT
END output
FROM stats,generate_series(1,stats.i) s;
Result:
output
---------
32.33.a1
32.33.a2
32.33.a3
32.33.a4
32.33.a5
32.34.a
32.34.b
32.34.c
32.35.b
32.35.c
32.35.d
32.35.e
(12 rows)

FMP 14 - Auto Populate a Field based on a calculation

I am using FMP 14 and would like to auto-populate field A based on the following calulation:
If ( Get ( ActiveLayoutObjectName ) = "tab_Visits_v1" ; "1st" ) or
If ( Get ( ActiveLayoutObjectName ) = "tab_Visits_v2" ; "2nd" ) or
If ( Get ( ActiveLayoutObjectName ) = "tab_Visits_v3" ; "3rd" ) or
If ( Get ( ActiveLayoutObjectName ) = "tab_Visits_v4" ; "4th" ) or
If ( Get ( ActiveLayoutObjectName ) = "tab_Visits_v5" ; "5th" ) or
If ( Get ( ActiveLayoutObjectName ) = "tab_Visits_v6" ; "6th" )
The above code is supposed to auto-populate the value 1st, 2nd, 3rd ... in field A depending on the name of the object the Get (ActiveLayoutObjectName) function returns. I have named each of my tabs, but the calculation is only returning 0.
Any help would be appreciated.
thanks.
The way your calculation is written makes very little sense. Each one of the If() statements returns a result of either "1st", "2nd", etc. or nothing (an empty string). You are then applying or to all these results. Since only of them can be true, your calculation is essentially doing something like:
"" or "2nd" or "" or "" or "" or ""
which happens to return 1 (true), but has no useful meaning.
You should be using the Case() function here:
Case (
Get ( ActiveLayoutObjectName ) = "tab_Visits_v1" ; "1st" ;
Get ( ActiveLayoutObjectName ) = "tab_Visits_v2" ; "2nd" ;
Get ( ActiveLayoutObjectName ) = "tab_Visits_v3" ; "3rd" ;
Get ( ActiveLayoutObjectName ) = "tab_Visits_v4" ; "4th" ;
Get ( ActiveLayoutObjectName ) = "tab_Visits_v5" ; "5th" ;
Get ( ActiveLayoutObjectName ) = "tab_Visits_v6" ; "6th"
)
Note also that a calculation field may not always refresh itself as a result of user switching a tab. This refers to an unstored calculation field; if you are trying to use this as the formula to be auto-entered into a "regular' (e.g. Text) field, it will never update.
Added:
Here is our situation. We see a patient a maximum of 6 times. We have
a tab for each one of those 6 visits.
I would suggest you use a portal to a related table of Visits instead of a tab control. A tab control is designed to display fixed components of user interface - not dynamic data. And certainly not data split into separate records. You should have only one unique record for each patient - and as many records for each patient's visits as may be necessary (potentially unlimited).
If you like, you can use the portal rows as buttons to select a specific visit to view in more detail (similar to a tab control, except that the portal shows the "tabs" as vertical rows). A one-row portal to the same Visits table, filtered by the user selection, would work very well for this purpose, I believe.
With 1. .... it would be easy:
Right (Get ( ActiveLayoutObjectName ) ; 1) & "."
Thanks for pointing out, that my first version does not work.