I've spent some time the last couple of months working on a side project: a volunteer tracking/scheduling/etc system for the boathouse where I volunteer. It's been fun, I've been learning more SQL, and CSS. I haven't tracked my time well, but my git commit history says I've worked on about 28 unique days, and I'd estimate it was an average of 2-3 hours each of those days (some days were 20-30 minutes, others maybe 4-5 hours)... so maybe around 60-90 hours of work. Plus White Tie's time making the CSS pretty(ier). Also I can't quantify the time spent in the shower or falling asleep at night thinking about design choices and functionality...
It's almost done, putting the finishing touches on it now. Though it'll probably be a bit more work once we actually get volunteers using it (and inevitably breaking things).
Some other stats:
--------------------------------------------------------------------------------
Language Files Lines Blank Comment Code
--------------------------------------------------------------------------------
PHP 11 3129 288 18 2823
CSS 1 440 30 0 410
JavaScript 1 43 2 0 41
--------------------------------------------------------------------------------
Total 13 3612 320 18 3274
--------------------------------------------------------------------------------
I tried to keep things as compact as possible, but also tried not to overdo the design. Started on PHP 5.5 on one of our dev boxes but now have fired up a VM that has PHP 8.x. Some crazy changes to inter-type comparisons in PHP 8. Eeep. But not so bad I guess once you start ditching some of the old bad habits.
Really appreciating how flexible SQL is. Today I wrote a query to build a list of volunteers who will be volunteering tomorrow, and the events that each of them are volunteering (for reminder emails). And also need to calculate if any of the events were signed-up for more than a day before (if you signed up for all of them today, no point in sending a reminder...):
SELECT users.email AS email,
(signed_up_at < DATE_SUB(NOW(), INTERVAL 1 DAY)) AS old,
events.*
FROM eventsignups
INNER JOIN events ON events.event_id = eventsignups.event_id
INNER JOIN users ON users.id = eventsignups.user_id
WHERE eventsignups.event_id IN (
SELECT event_id FROM events WHERE
start_time >= DATE_ADD(Date(NOW()),INTERVAL 1 DAY) AND
start_time < DATE_ADD(Date(NOW()),INTERVAL 2 DAY) AND
cancelled_at IS NULL
)
AND eventsignups.cancelled_at IS NULL
AND users.want_email_remind > 0
ORDER BY users.email;
(and then some PHP steps through the list by email to find all of the events for that email address, see if any have 'old' set, etc). Oddly satisfying.
Comment...
Today I finally got my ffmpeg incantation right on the first try... or so I thought. I left off the "k" for -b:v, boy was that output file tiny. Also TIL javascript has ` for multiline strings, nice.
Comment...
sometimes we have to fill out vendor forms in order to sell licenses to schools, which feels like such a waste of time. I always use Comic Sans. Just to make a point.
3 Comments
My 2024 stats (2023 was
here):
- Code: git commits from 7.07+dev1229 to 7.30+dev0108 which I am the purported author, excluding merges: 1,536 (way down from 2023's 2158)
- Code: cumulative changes in repository (not just me etc): 807 files changed, 80298 insertions(+), 58895 deletions(-)
- Foot-based-transport (from Strava): 2,814 miles in in 758 hours, including 191,548 feet of climbing (and descent). Of this 1,229 (207 hours) miles were running. Those numbers are up this year.
- Running: Ran a couple of marathons (SF and NY) in 3:31 and 3:25 or so. Ran my first 100k in January (posted previously), and also PR'd a half marathon at the Brooklyn Half in 1:28:49.
- Injuries: I didn't have any real injuries to speak of this year, yay, just some minor annoyances that made me slow down a bit at times (fussy plantar after the Brooklyn Half, ongoing glute pain -- though I feel like I'm finally getting that to improve).
- Bicycling: I strava'd about 1,037 miles. I didn't include some citibike bike angel shuffling rides and stuff though. Thinking I might do more rides in 2025.
- Bicycling: earned enough angel points to extend my membership
- Music played with others: 70 sessions (more than 2023 or 2021), producing about 106 hours of recordings.
- Music recorded: 18 doodles (down), 7 super8 sessions (also down).
The slowdown of getting old continues, I guess?
Comment...
Steve entered the Western States 100 lottery after qualifying in January... They are limited to 369 runners each year, and it's a legendary race, so there's a lot of demand. They have an interesting lottery structure: every year that you qualify and enter the lottery, if you are not picked, you get twice the entry tickets the next year.
After some early morning text messages where I tried (and to be honest, failed miserably) to calculate his odds, I wrote a little program in C to do the calculation.
Then, because sometimes programming is fun, I decided to see what it would look like in Perl. I haven't written a ton of Perl, I usually lean towards PHP for this sort of thing, but I ported SWELL's resource-processor script to Perl a while back and enjoyed it, and having a simple task is good for learning.
The first pass I did used a builtin array shuffle module, which proved too slow, then I ended up simplifying it and got it significantly faster than the C version (which didn't reshuffle, but did end up with pointless memmove()s). Once I had it all working (using strict, e.g. you have to declare everything), I decided to see how small I could get it to be. Here's what I ended up with (you can pipe the table on their lottery entry page to it):
$nwin = 250; # I think the lottery picked about 250-something winners last year, the rest were golden tickets and such
$nlot = 100000; # someone could quantify the margin of error based on this, with statistics you would oh look a bird
for ($pid = 0; <>; ) {
($nt, $np) = split(/\s+/, s/,//r);
($nt > 0 and $np > 0 and not exists $wcnt{$nt}) or die "invalid input: $_\n";
$wbin{$pid} = $nt;
$wcnt{$nt} = 0;
push(@tk, ($pid++) x $nt) while ($np-- > 0);
}
printf("%d tickets for %d entrants, running %d lotteries for %d winners:\n", $tkcnt = @tk, $pid, $nlot, $nwin);
for ($x = 0; $x < $nlot; $x++) {
%in = { };
$in{$id = $tk[rand($tkcnt)]}++ == 0 and exists $wbin{$id} and $wcnt{$wbin{$id}}++ while (%in < $nwin);
}
printf("%d tickets: %.2f%% win\n", $_, $wcnt{$_} * 100.0 / $nlot) foreach (sort { $a <=> $b } keys %wcnt);
Here is the most recent input:
512 1 512
256 15 3,840
128 55 7,040
64 139 8,896
32 215 6,880
16 296 4,736
8 594 4,752
4 963 3,852
2 1,538 3,076
1 2,077 2,077
and here is the most output with that table:
45661 tickets for 5893 entrants, running 100000 lotteries for 250 winners:
1 tickets: 0.66% win
2 tickets: 1.29% win
4 tickets: 2.56% win
8 tickets: 5.08% win
16 tickets: 9.99% win
32 tickets: 18.98% win
64 tickets: 34.12% win
128 tickets: 56.51% win
256 tickets: 80.91% win
512 tickets: 96.24% win
So, Steve's odds as of this afternoon are about 0.66%, but that will almost certainly go down (there's still a month left of the lottery; it only opened yesterday). Interestingly, one entrant there has been turned down 8 times before -- they currently have a 96% chance of getting in. And those who have been turnwed down 6 times before are slightly more likely than not to get in.
2 Comments
Microsoft generally does a very good job in preserving compatibility of existing applications; software written 20 years ago generally works well in Windows 11, amazingly. We noticed something they broke relating to menus that use bitmaps via MF_BITMAP, though, somewhere after Windows 7 -- I don't have 8.x installed anywhere to test, but Windows 10 and 11 are affected. Trying to figure out where to report these bugs online is difficult, and I'm lazy, so instead I'll document it here.
When setting a bitmap image for a menu item via MF_BITMAP, on Windows 10+, if the item does not have MF_CHECKED set, the bitmap will be drawn twice, and if the width of the menu is sufficiently longer than the width of the bitmap, one of those draws will be stretched. Here's some code that reproduces:
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE prev, LPSTR cmd, int ns)
{
const int w = 32, h = 16;
int pixels[w*h];
for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) pixels[x + y * w] = RGB(x * 256 / w, y * 256 / h, 0); // oops BGR but whatever
HBITMAP bitmap = CreateBitmap(w, h, 1, 32, &pixels);
HWND wnd = CreateWindowEx(0, "Edit", "test", WS_VISIBLE, 0, 0, 10, 10, GetDesktopWindow(), NULL, hInst, 0);
HMENU menu = CreatePopupMenu();
InsertMenuA(menu, 0, MF_BYPOSITION|MF_STRING, 1, (char*)"this is a thing that will cause stretching to happen");
InsertMenuA(menu, 1, MF_BYPOSITION|MF_BITMAP, 2, (char*)bitmap);
TrackPopupMenu(menu, TPM_NONOTIFY|TPM_RETURNCMD, 0, 0, 0, wnd, NULL);
DestroyMenu(menu);
DeleteObject(bitmap);
DestroyWindow(wnd);
return 0;
}
Windows XP, Windows 7, and WINE all display something about like this, which is expected:
Windows 10 and 11 display this:
As a workaround, you can call SetMenuItemBitmaps() for the item with hBitmapUnchecked to a small image, and it will prevent the bug from occurring (or at least prevent it from being obvious heh). It was a waste of time writing this blog post and writing the test case, but the things we do for fun, right?
3 Comments
I've updated this web page to use more CSS and removed most of the tables and stuff. I also got rid of the infinity scroll because
it was just silly.
that is all.
p.s. the juice carrots take immediately below this post is something I like, now I have to find a way to fit a chorus pedal on my pedalboard
Comment...
there are very few things more satisfying than cutting a slot in the previously stripped head of a screw using a dremel and then using a flathead screwdriver to remove it
3 Comments
When I first started programming C++ in the 1990s, there were language features that I found appalling. One of those was operator overloading, which was used in the most basic of C++ example code, e.g.
cout << "hello, world" << endl;
This made the C programmer in me squirm. Why would you make the meaning of operators change wildly based on the context? Also you might lose track of what code is actually being generated. It could have side effects that you don't know about, or be orders of magnitude slower! I still fill this way, and avoid operator overloading, other than operator=, for most things.
...but having said that, I find operator overloading to be incredibly valuable when it comes to maintaining and refactoring a large code base. A pattern that has become routine for us is a basic type is initially used for some state and needs to be extended.
For example: in track groups, we originally had 32 groups (1-32), and a track could have different types of membership in any number of groups. For 32 groups, we used unsigned ints as bitmasks. Some years later, to support 64 groups we changed it to WDL_UINT64 (aka uint64_t). Straightforward (but actually quite tedious and in hindsight we should've skipped the type change and gone right to the next step). To increase beyond 64 bits, there's no longer a basic type that can be used. So instead:
struct group_membership {
enum { SZ = 2 };
WDL_UINT64 m_data[SZ];
const group_membership operator & (const group_membership &o) const
{
group_membership r;
for (int i = 0; i < SZ; i ++) r.m_data[i] = m_data[i] & o.m_data[i];
return r;
}
// and a bunch of other operators, a couple of functions to clear/set common masks, etc
private:
// prevent direct cast to int/int64/etc. necessary because we allow casting to bool below which would otherwise be quietly promoted to int
operator int() const { return 0; }
public:
operator bool() const { for (int i = 0; i < SZ; i ++) if (m_data[i]) return true; return false; }
};
Then we replace things like:
WDL_UINT64 group;
with
group_membership group;
and after that, (assuming you get all of the necessary operators mentioned in the comment above) most code just works without modification, e.g.:
if (group & group_mask) { /* ... */ }
To be fair, we could easy tweak all of the code that the operator overloading implements to use functions, and not do this, but for me knowing that I'm not screwing up the logic in some subtle way is a big win. And if you have multiple branches and you're worried about merge conflicts, this avoids a lot of them.
Also, it's fun to look at the output of gcc or clang on these things. They end up producing pretty much optimal code, even when returning and copying structs. Though you should be sure to keep the struct in question as plain-old-data (ideally no constructor, or a trivial constructor, and no destructor).
Thus concludes my "a thing I appreciate about C++" talk. Next week, templates.
Recordings:
super8_novideo - 1 -- [9:36]
super8_novideo - 2 -- [4:16]
super8_novideo - 3 -- [4:34]
super8_novideo - 4 -- [8:20]
12 Comments