Skip to content

Commit 78d950c

Browse files
Merge branch 'master' into feat-hierholzer-algorithm
2 parents a62a7c4 + 0837424 commit 78d950c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+4997
-134
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
name: Close stale PRs with failed workflows
2+
3+
on:
4+
schedule:
5+
- cron: '0 3 * * *' # runs daily at 03:00 UTC
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
issues: write
11+
pull-requests: write
12+
13+
jobs:
14+
close-stale:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Close stale PRs
18+
uses: actions/github-script@v8
19+
with:
20+
github-token: ${{ secrets.GITHUB_TOKEN }}
21+
script: |
22+
const mainBranches = ['main', 'master'];
23+
const cutoffDays = 14;
24+
const cutoff = new Date();
25+
cutoff.setDate(cutoff.getDate() - cutoffDays);
26+
27+
console.log(`Checking PRs older than: ${cutoff.toISOString()}`);
28+
29+
try {
30+
const { data: prs } = await github.rest.pulls.list({
31+
owner: context.repo.owner,
32+
repo: context.repo.repo,
33+
state: 'open',
34+
sort: 'updated',
35+
direction: 'asc',
36+
per_page: 100
37+
});
38+
39+
console.log(`Found ${prs.length} open PRs to check`);
40+
41+
for (const pr of prs) {
42+
try {
43+
const updated = new Date(pr.updated_at);
44+
45+
if (updated > cutoff) {
46+
console.log(`⏩ Skipping PR #${pr.number} - updated recently`);
47+
continue;
48+
}
49+
50+
console.log(`🔍 Checking PR #${pr.number}: "${pr.title}"`);
51+
52+
// Get commits
53+
const commits = await github.paginate(github.rest.pulls.listCommits, {
54+
owner: context.repo.owner,
55+
repo: context.repo.repo,
56+
pull_number: pr.number,
57+
per_page: 100
58+
});
59+
60+
const meaningfulCommits = commits.filter(c => {
61+
const msg = c.commit.message.toLowerCase();
62+
const isMergeFromMain = mainBranches.some(branch =>
63+
msg.startsWith(`merge branch '${branch}'`) ||
64+
msg.includes(`merge remote-tracking branch '${branch}'`)
65+
);
66+
return !isMergeFromMain;
67+
});
68+
69+
// Get checks with error handling
70+
let hasFailedChecks = false;
71+
let allChecksCompleted = false;
72+
let hasChecks = false;
73+
74+
try {
75+
const { data: checks } = await github.rest.checks.listForRef({
76+
owner: context.repo.owner,
77+
repo: context.repo.repo,
78+
ref: pr.head.sha
79+
});
80+
81+
hasChecks = checks.check_runs.length > 0;
82+
hasFailedChecks = checks.check_runs.some(c => c.conclusion === 'failure');
83+
allChecksCompleted = checks.check_runs.every(c =>
84+
c.status === 'completed' || c.status === 'skipped'
85+
);
86+
} catch (error) {
87+
console.log(`⚠️ Could not fetch checks for PR #${pr.number}: ${error.message}`);
88+
}
89+
90+
// Get workflow runs with error handling
91+
let hasFailedWorkflows = false;
92+
let allWorkflowsCompleted = false;
93+
let hasWorkflows = false;
94+
95+
try {
96+
const { data: runs } = await github.rest.actions.listWorkflowRuns({
97+
owner: context.repo.owner,
98+
repo: context.repo.repo,
99+
head_sha: pr.head.sha,
100+
per_page: 50
101+
});
102+
103+
hasWorkflows = runs.workflow_runs.length > 0;
104+
hasFailedWorkflows = runs.workflow_runs.some(r => r.conclusion === 'failure');
105+
allWorkflowsCompleted = runs.workflow_runs.every(r =>
106+
['completed', 'skipped', 'cancelled'].includes(r.status)
107+
);
108+
109+
console.log(`PR #${pr.number}: ${runs.workflow_runs.length} workflow runs found`);
110+
111+
} catch (error) {
112+
console.log(`⚠️ Could not fetch workflow runs for PR #${pr.number}: ${error.message}`);
113+
}
114+
115+
console.log(`PR #${pr.number}: ${meaningfulCommits.length} meaningful commits`);
116+
console.log(`Checks - has: ${hasChecks}, failed: ${hasFailedChecks}, completed: ${allChecksCompleted}`);
117+
console.log(`Workflows - has: ${hasWorkflows}, failed: ${hasFailedWorkflows}, completed: ${allWorkflowsCompleted}`);
118+
119+
// Combine conditions - only consider if we actually have checks/workflows
120+
const hasAnyFailure = (hasChecks && hasFailedChecks) || (hasWorkflows && hasFailedWorkflows);
121+
const allCompleted = (!hasChecks || allChecksCompleted) && (!hasWorkflows || allWorkflowsCompleted);
122+
123+
if (meaningfulCommits.length === 0 && hasAnyFailure && allCompleted) {
124+
console.log(`✅ Closing PR #${pr.number} (${pr.title})`);
125+
126+
await github.rest.issues.createComment({
127+
owner: context.repo.owner,
128+
repo: context.repo.repo,
129+
issue_number: pr.number,
130+
body: `This pull request has been automatically closed because its workflows or checks failed and it has been inactive for more than ${cutoffDays} days. Please fix the workflows and reopen if you'd like to continue. Merging from main/master alone does not count as activity.`
131+
});
132+
133+
await github.rest.pulls.update({
134+
owner: context.repo.owner,
135+
repo: context.repo.repo,
136+
pull_number: pr.number,
137+
state: 'closed'
138+
});
139+
140+
console.log(`✅ Successfully closed PR #${pr.number}`);
141+
} else {
142+
console.log(`⏩ Not closing PR #${pr.number} - conditions not met`);
143+
}
144+
145+
} catch (prError) {
146+
console.error(`❌ Error processing PR #${pr.number}: ${prError.message}`);
147+
continue;
148+
}
149+
}
150+
151+
} catch (error) {
152+
console.error(`❌ Fatal error: ${error.message}`);
153+
throw error;
154+
}

pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<dependency>
2121
<groupId>org.junit</groupId>
2222
<artifactId>junit-bom</artifactId>
23-
<version>6.0.0</version>
23+
<version>6.0.1</version>
2424
<type>pom</type>
2525
<scope>import</scope>
2626
</dependency>
@@ -112,14 +112,14 @@
112112
<dependency>
113113
<groupId>com.puppycrawl.tools</groupId>
114114
<artifactId>checkstyle</artifactId>
115-
<version>12.0.1</version>
115+
<version>12.1.1</version>
116116
</dependency>
117117
</dependencies>
118118
</plugin>
119119
<plugin>
120120
<groupId>com.github.spotbugs</groupId>
121121
<artifactId>spotbugs-maven-plugin</artifactId>
122-
<version>4.9.7.0</version>
122+
<version>4.9.8.1</version>
123123
<configuration>
124124
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
125125
<includeTests>true</includeTests>
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.thealgorithms.compression;
2+
3+
import java.math.BigDecimal;
4+
import java.math.MathContext;
5+
import java.util.ArrayList;
6+
import java.util.Collections;
7+
import java.util.HashMap;
8+
import java.util.List;
9+
import java.util.Map;
10+
11+
/**
12+
* An implementation of the Arithmetic Coding algorithm.
13+
*
14+
* <p>
15+
* Arithmetic coding is a form of entropy encoding used in lossless data
16+
* compression. It encodes an entire message into a single number, a fraction n
17+
* where (0.0 <= n < 1.0). Unlike Huffman coding, which assigns a specific
18+
* bit sequence to each symbol, arithmetic coding represents the message as a
19+
* sub-interval of the [0, 1) interval.
20+
* </p>
21+
*
22+
* <p>
23+
* This implementation uses BigDecimal for precision to handle the shrinking
24+
* intervals, making it suitable for educational purposes to demonstrate the
25+
* core logic.
26+
* </p>
27+
*
28+
* <p>
29+
* Time Complexity: O(n*m) for compression and decompression where n is the
30+
* length of the input and m is the number of unique symbols, due to the need
31+
* to calculate symbol probabilities.
32+
* </p>
33+
*
34+
* <p>
35+
* References:
36+
* <ul>
37+
* <li><a href="https://en.wikipedia.org/wiki/Arithmetic_coding">Wikipedia:
38+
* Arithmetic coding</a></li>
39+
* </ul>
40+
* </p>
41+
*/
42+
public final class ArithmeticCoding {
43+
44+
private ArithmeticCoding() {
45+
}
46+
47+
/**
48+
* Compresses a string using the Arithmetic Coding algorithm.
49+
*
50+
* @param uncompressed The string to be compressed.
51+
* @return The compressed representation as a BigDecimal number.
52+
* @throws IllegalArgumentException if the input string is null or empty.
53+
*/
54+
public static BigDecimal compress(String uncompressed) {
55+
if (uncompressed == null || uncompressed.isEmpty()) {
56+
throw new IllegalArgumentException("Input string cannot be null or empty.");
57+
}
58+
59+
Map<Character, Symbol> probabilityTable = calculateProbabilities(uncompressed);
60+
61+
BigDecimal low = BigDecimal.ZERO;
62+
BigDecimal high = BigDecimal.ONE;
63+
64+
for (char symbol : uncompressed.toCharArray()) {
65+
BigDecimal range = high.subtract(low);
66+
Symbol sym = probabilityTable.get(symbol);
67+
68+
high = low.add(range.multiply(sym.high()));
69+
low = low.add(range.multiply(sym.low()));
70+
}
71+
72+
return low; // Return the lower bound of the final interval
73+
}
74+
75+
/**
76+
* Decompresses a BigDecimal number back into the original string.
77+
*
78+
* @param compressed The compressed BigDecimal number.
79+
* @param length The length of the original uncompressed string.
80+
* @param probabilityTable The probability table used during compression.
81+
* @return The original, uncompressed string.
82+
*/
83+
public static String decompress(BigDecimal compressed, int length, Map<Character, Symbol> probabilityTable) {
84+
StringBuilder decompressed = new StringBuilder();
85+
86+
// Create a sorted list of symbols for deterministic decompression, matching the
87+
// order used in calculateProbabilities
88+
List<Map.Entry<Character, Symbol>> sortedSymbols = new ArrayList<>(probabilityTable.entrySet());
89+
sortedSymbols.sort(Map.Entry.comparingByKey());
90+
91+
BigDecimal low = BigDecimal.ZERO;
92+
BigDecimal high = BigDecimal.ONE;
93+
94+
for (int i = 0; i < length; i++) {
95+
BigDecimal range = high.subtract(low);
96+
97+
// Find which symbol the compressed value falls into
98+
for (Map.Entry<Character, Symbol> entry : sortedSymbols) {
99+
Symbol sym = entry.getValue();
100+
101+
// Calculate the actual range for this symbol in the current interval
102+
BigDecimal symLow = low.add(range.multiply(sym.low()));
103+
BigDecimal symHigh = low.add(range.multiply(sym.high()));
104+
105+
// Check if the compressed value falls within this symbol's range
106+
if (compressed.compareTo(symLow) >= 0 && compressed.compareTo(symHigh) < 0) {
107+
decompressed.append(entry.getKey());
108+
109+
// Update the interval for the next iteration
110+
low = symLow;
111+
high = symHigh;
112+
break;
113+
}
114+
}
115+
}
116+
117+
return decompressed.toString();
118+
}
119+
120+
/**
121+
* Calculates the frequency and probability range for each character in the
122+
* input string in a deterministic order.
123+
*
124+
* @param text The input string.
125+
* @return A map from each character to a Symbol object containing its
126+
* probability range.
127+
*/
128+
public static Map<Character, Symbol> calculateProbabilities(String text) {
129+
Map<Character, Integer> frequencies = new HashMap<>();
130+
for (char c : text.toCharArray()) {
131+
frequencies.put(c, frequencies.getOrDefault(c, 0) + 1);
132+
}
133+
134+
// Sort the characters to ensure a deterministic order for the probability table
135+
List<Character> sortedKeys = new ArrayList<>(frequencies.keySet());
136+
Collections.sort(sortedKeys);
137+
138+
Map<Character, Symbol> probabilityTable = new HashMap<>();
139+
BigDecimal currentLow = BigDecimal.ZERO;
140+
int total = text.length();
141+
142+
for (char symbol : sortedKeys) {
143+
BigDecimal probability = BigDecimal.valueOf(frequencies.get(symbol)).divide(BigDecimal.valueOf(total), MathContext.DECIMAL128);
144+
BigDecimal high = currentLow.add(probability);
145+
probabilityTable.put(symbol, new Symbol(currentLow, high));
146+
currentLow = high;
147+
}
148+
149+
return probabilityTable;
150+
}
151+
152+
/**
153+
* Helper class to store the probability range [low, high) for a symbol.
154+
*/
155+
public record Symbol(BigDecimal low, BigDecimal high) {
156+
}
157+
}

0 commit comments

Comments
 (0)