churn_vs_complexity 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +12 -2
  4. data/bin/churn_vs_complexity +6 -1
  5. data/lib/churn_vs_complexity/churn.rb +17 -2
  6. data/lib/churn_vs_complexity/cli/parser.rb +8 -0
  7. data/lib/churn_vs_complexity/complexity/kotlin_calculator.rb +1 -1
  8. data/lib/churn_vs_complexity/complexity/rust_calculator.rb +54 -0
  9. data/lib/churn_vs_complexity/complexity/swift_calculator.rb +54 -0
  10. data/lib/churn_vs_complexity/complexity.rb +2 -0
  11. data/lib/churn_vs_complexity/complexity_validator.rb +4 -0
  12. data/lib/churn_vs_complexity/delta/serializer.rb +1 -0
  13. data/lib/churn_vs_complexity/delta.rb +8 -0
  14. data/lib/churn_vs_complexity/file_selector.rb +24 -0
  15. data/lib/churn_vs_complexity/gate/config.rb +7 -0
  16. data/lib/churn_vs_complexity/gate/serializer.rb +9 -2
  17. data/lib/churn_vs_complexity/git_strategy.rb +16 -2
  18. data/lib/churn_vs_complexity/language_validator.rb +1 -1
  19. data/lib/churn_vs_complexity/normal/config.rb +16 -0
  20. data/lib/churn_vs_complexity/normal/serializer/summary_hash.rb +21 -8
  21. data/lib/churn_vs_complexity/risk_classifier.rb +2 -0
  22. data/lib/churn_vs_complexity/timetravel/traveller.rb +2 -0
  23. data/lib/churn_vs_complexity/version.rb +1 -1
  24. metadata +11 -42
  25. data/.devcontainer/devcontainer.json +0 -26
  26. data/.rubocop.yml +0 -25
  27. data/CLAUDE.md +0 -60
  28. data/Dockerfile +0 -34
  29. data/Dockerfile.dev +0 -27
  30. data/Rakefile +0 -6
  31. data/TODO +0 -11
  32. data/package-lock.json +0 -6
  33. data/tmp/test-support/delta/ruby-summary.txt +0 -55
  34. data/tmp/test-support/delta/ruby.csv +0 -12
  35. data/tmp/test-support/go/main.go +0 -11
  36. data/tmp/test-support/go/utils.go +0 -13
  37. data/tmp/test-support/java/small-example/src/main/java/org/example/Main.java +0 -10
  38. data/tmp/test-support/java/small-example/src/main/java/org/example/spice/Checker.java +0 -28
  39. data/tmp/test-support/javascript/complex.js +0 -43
  40. data/tmp/test-support/javascript/moderate.js +0 -12
  41. data/tmp/test-support/javascript/simple.js +0 -5
  42. data/tmp/test-support/javascript/typescript-example.ts +0 -26
  43. data/tmp/test-support/kotlin/Main.kt +0 -7
  44. data/tmp/test-support/kotlin/Utils.kt +0 -11
  45. data/tmp/test-support/python/example.py +0 -6
  46. data/tmp/test-support/python/utils.py +0 -9
  47. data/tmp/test-support/txt/abc.txt +0 -0
  48. data/tmp/test-support/txt/d.txt +0 -0
  49. data/tmp/test-support/txt/ef.txt +0 -0
  50. data/tmp/test-support/txt/ghij.txt +0 -0
  51. data/tmp/test-support/txt/klm.txt +0 -0
  52. data/tmp/test-support/txt/nopq.txt +0 -0
  53. data/tmp/test-support/txt/r.txt +0 -0
  54. data/tmp/test-support/txt/st.txt +0 -0
  55. data/tmp/test-support/txt/uvx.txt +0 -0
  56. data/tmp/test-support/txt/yz.txt +0 -0
  57. data/tmp/timetravel/.keep +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbfecd03ad25cb8f79ae5b52564f591b122eab2b8ee13e3fe80d1d33883e2747
4
- data.tar.gz: 4ba6ffb8794bb11c9e87103f26b481b9b6bb29230eb1c55fa7fff7c46aaa0687
3
+ metadata.gz: 612c6e1945c04f5be2c72cdbdfbd4bc91157da01c6d4ab66f5a502a7e109c805
4
+ data.tar.gz: c84417b6b7eeeeb4fac050e32c89f037b010977c342e8db14762d28b2f63d557
5
5
  SHA512:
6
- metadata.gz: c7463ac718037f9f8d13ba08d1c39b775557246f85bd3e29852ee573b1af55625026e792d239e3d38df118923f80b68d75243508f5dd98b6739bd1b80408fe20
7
- data.tar.gz: 35358bac1704bc9bca9cb9204d053c3484d9aa5e4d1e378b0feb48e2236f014354494b8ddcd36d12d127865361add543f511e514bfa412d01f7b34f038fb1042
6
+ metadata.gz: a8d9617c65744a1b04910c0e9206d788cddcc02133820a05d9bad5bdc160d9c11e05552080bee72d2a1a9b53c765e884cd249095e54a866e38cdb6f3b803880e
7
+ data.tar.gz: 7174c9a6d71d2db643b83b72c49973aef249b09aafacc41112e25407b58a3a46675af29f968c5f84226b8e53fb114c3546664f4f9c2b35940277d68d8f9e5fda
data/CHANGELOG.md CHANGED
@@ -1,3 +1,48 @@
1
+ ## [1.9.0] - 2026-09-20
2
+
3
+ ### Fixed
4
+ - Churn counted only the commits made since the 30th most recent commit. The
5
+ underlying `git` gem caps a log at 30 entries by default, and the clamp that
6
+ is meant to stop `--since` reaching past the first commit was reading that cap
7
+ as the first commit. Any repository with more than 30 commits was undercounted.
8
+ - Timetravel built its commit window from bare dates, which git treats as
9
+ approximate and resolves against the clock and the timezone. Commits on the
10
+ first and last day of the period were included or dropped depending on when
11
+ and where the command ran. Both ends are now given an explicit time.
12
+ - Running any mode against a repository with no commits raised
13
+ `NoMethodError: undefined method 'date' for nil`. It now says which folder has
14
+ no commits.
15
+ - Errors the tool raises on purpose, such as telling you which analyser to
16
+ install, were printed underneath a backtrace. The backtrace is now kept for
17
+ genuinely unexpected failures. These still exit 2, so a gate failure (exit 1)
18
+ stays distinguishable from a tool that could not run.
19
+ - `--gate --max-gamma 0` crashed with `FloatDomainError: Infinity`, because the
20
+ percentage by which a score exceeds the threshold divided by it. Violations are
21
+ now reported against a zero threshold with `exceeds_by` set to null.
22
+ - A negative `--max-gamma` is now rejected instead of reporting every file as a
23
+ violation with a negative excess.
24
+ - Timetravel raised `ArgumentError: invalid slice size` when no commits fell in
25
+ the requested period. An empty period now produces an empty result.
26
+ - The `churn_vs_complexity` executable failed with a `LoadError` when run from
27
+ inside a project that uses Bundler, which is most of the repositories the tool
28
+ exists to analyse. It required `bundler/setup`, so it resolved against the
29
+ analysed project's Gemfile instead of its own installed gems.
30
+
31
+ ### Changed
32
+ - The `git` dependency moves from 2.x to 5.x and `flog` from 4.8 to 4.9.
33
+ - The published gem no longer carries the test fixtures, the Dockerfiles or the
34
+ other development files it had been shipping.
35
+
36
+ ### Documentation
37
+ - The README told Go users to install gocyclo. The Go calculator runs gocognit,
38
+ and has since Go support was added.
39
+
40
+ ## [1.8.0] - 2026-04-05
41
+
42
+ ### Added
43
+ - Rust support for complexity calculation (via [lizard](https://github.com/terryyin/lizard), install with `pip install lizard`)
44
+ - Swift support for complexity calculation (via [lizard](https://github.com/terryyin/lizard), install with `pip install lizard`)
45
+
1
46
  ## [1.7.0] - 2026-04-05
2
47
 
3
48
  ### Added
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # ChurnVsComplexity
4
4
 
5
- Correlates file churn (how often files change) with complexity scores to identify refactoring hotspots and track codebase health over time. Supports Ruby, JavaScript/TypeScript, Java, Python, Go, and Kotlin.
5
+ Correlates file churn (how often files change) with complexity scores to identify refactoring hotspots and track codebase health over time. Supports Ruby, JavaScript/TypeScript, Java, Python, Go, Kotlin, Rust, and Swift.
6
6
 
7
7
  Modes include hotspots ranking, triage assessment, CI quality gate, diff comparison, focus sessions, and timetravel history.
8
8
 
@@ -32,8 +32,10 @@ External tool dependencies per language:
32
32
  - **Java**: Requires [PMD](https://pmd.github.io) on the search path as `pmd`. On macOS: `brew install pmd`.
33
33
  - **JavaScript/TypeScript**: Requires [Node.js](https://nodejs.org) (uses ESLint internally).
34
34
  - **Python**: Requires [Radon](https://radon.readthedocs.io) on the search path as `radon`. Install with `pip install radon`.
35
- - **Go**: Requires [gocyclo](https://github.com/fzipp/gocyclo) on the search path. Install with `go install github.com/fzipp/gocyclo/cmd/gocyclo@latest`.
35
+ - **Go**: Requires [gocognit](https://github.com/uudashr/gocognit) on the search path. Install with `go install github.com/uudashr/gocognit/cmd/gocognit@latest`.
36
36
  - **Kotlin**: Requires [lizard](https://github.com/terryyin/lizard) on the search path. Install with `pip install lizard`.
37
+ - **Rust**: Requires [lizard](https://github.com/terryyin/lizard) on the search path. Install with `pip install lizard`.
38
+ - **Swift**: Requires [lizard](https://github.com/terryyin/lizard) on the search path. Install with `pip install lizard`.
37
39
 
38
40
  ## Usage
39
41
 
@@ -50,6 +52,8 @@ Languages:
50
52
  --python Check complexity of python files
51
53
  --go Check complexity of go files
52
54
  --kotlin Check complexity of kotlin files
55
+ --rust Check complexity of rust files
56
+ --swift Check complexity of swift files
53
57
 
54
58
  Modes (mutually exclusive):
55
59
  --timetravel N Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since
@@ -126,6 +130,12 @@ churn_vs_complexity --js --delta HEAD --summary my_js_project
126
130
 
127
131
  # Kotlin project hotspots
128
132
  churn_vs_complexity --kotlin --hotspots my_kotlin_project
133
+
134
+ # Rust project CSV report
135
+ churn_vs_complexity --rust --csv my_rust_project > ~/Desktop/rust-demo.csv
136
+
137
+ # Swift project summary
138
+ churn_vs_complexity --swift --summary -q my_swift_project
129
139
  ```
130
140
 
131
141
  ## Development
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'bundler/setup'
5
4
  require 'churn_vs_complexity'
6
5
 
7
6
  begin
@@ -9,6 +8,12 @@ begin
9
8
  rescue ChurnVsComplexity::ValidationError => e
10
9
  warn e.message
11
10
  exit 1
11
+ rescue ChurnVsComplexity::Error => e
12
+ # Raised deliberately, so the message is the point and a backtrace only
13
+ # buries it. Exit 2, because exit 1 is how the gate reports a codebase that
14
+ # failed its threshold.
15
+ warn e.message
16
+ exit 2
12
17
  rescue StandardError => e
13
18
  warn e.backtrace
14
19
  warn e.message
@@ -23,13 +23,28 @@ module ChurnVsComplexity
23
23
  end
24
24
 
25
25
  def date_of_latest_commit(folder:)
26
- repo(folder).log.first.date
26
+ latest = repo(folder).log(1).execute.first
27
+ raise Error, "#{folder} has no commits to measure churn against" if latest.nil?
28
+
29
+ latest.date
27
30
  end
28
31
 
29
32
  private
30
33
 
34
+ # Asks git for the root commits directly. Reading it off `repo(folder).log`
35
+ # instead would silently answer with the 30th most recent commit, because
36
+ # that is where the git gem caps a log by default.
31
37
  def date_of_first_commit(folder:)
32
- repo(folder).log.last&.date&.to_date || Time.at(0).to_date
38
+ git_dir = File.join(folder, '.git')
39
+ cmd = %(git --git-dir #{git_dir} --work-tree #{folder} log --max-parents=0 --format=%ad --date=short)
40
+ dates = `(#{cmd}) 2>/dev/null`.split("\n").filter_map { |line| parse_date(line) }
41
+ dates.min || Time.at(0).to_date
42
+ end
43
+
44
+ def parse_date(line)
45
+ Date.parse(line)
46
+ rescue Date::Error
47
+ nil
33
48
  end
34
49
 
35
50
  def repo(folder)
@@ -38,6 +38,14 @@ module ChurnVsComplexity
38
38
  options[:language] = :kotlin
39
39
  end
40
40
 
41
+ opts.on('--rust', 'Check complexity of rust files') do
42
+ options[:language] = :rust
43
+ end
44
+
45
+ opts.on('--swift', 'Check complexity of swift files') do
46
+ options[:language] = :swift
47
+ end
48
+
41
49
  opts.separator ''
42
50
  opts.separator 'Modes (mutually exclusive):'
43
51
 
@@ -16,7 +16,7 @@ module ChurnVsComplexity
16
16
  end
17
17
 
18
18
  # Lizard CSV format: NLOC,CCN,tokens,params,length,"loc","file","func","long_func",start,end
19
- LIZARD_LINE_PATTERN = /^\d+,(\d+),\d+,\d+,\d+,"[^"]*","([^"]*)"/.freeze
19
+ LIZARD_LINE_PATTERN = /^\d+,(\d+),\d+,\d+,\d+,"[^"]*","([^"]*)"/
20
20
 
21
21
  def parse_lizard_output(csv_output, files:)
22
22
  scores = Hash.new(0)
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module ChurnVsComplexity
6
+ module Complexity
7
+ module RustCalculator
8
+ class << self
9
+ attr_writer :command_runner
10
+
11
+ def folder_based? = false
12
+
13
+ def calculate(files:)
14
+ csv_output = run_lizard(files)
15
+ parse_lizard_output(csv_output, files:)
16
+ end
17
+
18
+ # Lizard CSV format: NLOC,CCN,tokens,params,length,"loc","file","func","long_func",start,end
19
+ LIZARD_LINE_PATTERN = /^\d+,(\d+),\d+,\d+,\d+,"[^"]*","([^"]*)"/
20
+
21
+ def parse_lizard_output(csv_output, files:)
22
+ scores = Hash.new(0)
23
+ csv_output.each_line do |line|
24
+ match = line.match(LIZARD_LINE_PATTERN)
25
+ next unless match
26
+
27
+ scores[match[2]] += match[1].to_i
28
+ end
29
+ files.to_h { |file| [file, scores[file] || 0] }
30
+ end
31
+
32
+ def check_dependencies!
33
+ command_runner.call('lizard --version 2>&1')
34
+ rescue Errno::ENOENT
35
+ raise Error, 'Needs lizard installed (pip install lizard)'
36
+ end
37
+
38
+ private
39
+
40
+ def command_runner
41
+ @command_runner || Open3.method(:capture2)
42
+ end
43
+
44
+ def run_lizard(files)
45
+ files_arg = files.map { |f| "'#{f}'" }.join(' ')
46
+ stdout, status = command_runner.call("lizard --csv #{files_arg}")
47
+ raise Error, "lizard failed (exit #{status.exitstatus}). Is it installed?" unless status.success?
48
+
49
+ stdout
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module ChurnVsComplexity
6
+ module Complexity
7
+ module SwiftCalculator
8
+ class << self
9
+ attr_writer :command_runner
10
+
11
+ def folder_based? = false
12
+
13
+ def calculate(files:)
14
+ csv_output = run_lizard(files)
15
+ parse_lizard_output(csv_output, files:)
16
+ end
17
+
18
+ # Lizard CSV format: NLOC,CCN,tokens,params,length,"loc","file","func","long_func",start,end
19
+ LIZARD_LINE_PATTERN = /^\d+,(\d+),\d+,\d+,\d+,"[^"]*","([^"]*)"/
20
+
21
+ def parse_lizard_output(csv_output, files:)
22
+ scores = Hash.new(0)
23
+ csv_output.each_line do |line|
24
+ match = line.match(LIZARD_LINE_PATTERN)
25
+ next unless match
26
+
27
+ scores[match[2]] += match[1].to_i
28
+ end
29
+ files.to_h { |file| [file, scores[file] || 0] }
30
+ end
31
+
32
+ def check_dependencies!
33
+ command_runner.call('lizard --version 2>&1')
34
+ rescue Errno::ENOENT
35
+ raise Error, 'Needs lizard installed (pip install lizard)'
36
+ end
37
+
38
+ private
39
+
40
+ def command_runner
41
+ @command_runner || Open3.method(:capture2)
42
+ end
43
+
44
+ def run_lizard(files)
45
+ files_arg = files.map { |f| "'#{f}'" }.join(' ')
46
+ stdout, status = command_runner.call("lizard --csv #{files_arg}")
47
+ raise Error, "lizard failed (exit #{status.exitstatus}). Is it installed?" unless status.success?
48
+
49
+ stdout
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -6,6 +6,8 @@ require_relative 'complexity/eslint_calculator'
6
6
  require_relative 'complexity/python_calculator'
7
7
  require_relative 'complexity/go_calculator'
8
8
  require_relative 'complexity/kotlin_calculator'
9
+ require_relative 'complexity/rust_calculator'
10
+ require_relative 'complexity/swift_calculator'
9
11
 
10
12
  module ChurnVsComplexity
11
13
  module Complexity
@@ -14,6 +14,10 @@ module ChurnVsComplexity
14
14
  Complexity::GoCalculator.check_dependencies!
15
15
  when :kotlin
16
16
  Complexity::KotlinCalculator.check_dependencies!
17
+ when :rust
18
+ Complexity::RustCalculator.check_dependencies!
19
+ when :swift
20
+ Complexity::SwiftCalculator.check_dependencies!
17
21
  end
18
22
  end
19
23
  end
@@ -62,6 +62,7 @@ module ChurnVsComplexity
62
62
 
63
63
  module PassThrough
64
64
  extend Normal::Serializer::None
65
+
65
66
  def self.has_commit_summary? = true
66
67
  end
67
68
  end
@@ -40,6 +40,10 @@ module ChurnVsComplexity
40
40
  FileSelector::Go.predefined(included:, excluded:)
41
41
  when :kotlin
42
42
  FileSelector::Kotlin.predefined(included:, excluded:)
43
+ when :rust
44
+ FileSelector::Rust.predefined(included:, excluded:)
45
+ when :swift
46
+ FileSelector::Swift.predefined(included:, excluded:)
43
47
  end
44
48
  end
45
49
 
@@ -57,6 +61,10 @@ module ChurnVsComplexity
57
61
  Complexity::GoCalculator
58
62
  when :kotlin
59
63
  Complexity::KotlinCalculator
64
+ when :rust
65
+ Complexity::RustCalculator
66
+ when :swift
67
+ Complexity::SwiftCalculator
60
68
  end
61
69
  end
62
70
  end
@@ -16,6 +16,10 @@ module ChurnVsComplexity
16
16
  ['.go']
17
17
  when :kotlin
18
18
  ['.kt', '.kts']
19
+ when :rust
20
+ ['.rs']
21
+ when :swift
22
+ ['.swift']
19
23
  else
20
24
  raise Error, "Unsupported language: #{language}"
21
25
  end
@@ -142,5 +146,25 @@ module ChurnVsComplexity
142
146
  Predefined.new(included:, extensions: FileSelector.extensions(:kotlin), excluded:)
143
147
  end
144
148
  end
149
+
150
+ module Rust
151
+ def self.excluding(excluded)
152
+ Excluding.new(FileSelector.extensions(:rust), excluded)
153
+ end
154
+
155
+ def self.predefined(included:, excluded:)
156
+ Predefined.new(included:, extensions: FileSelector.extensions(:rust), excluded:)
157
+ end
158
+ end
159
+
160
+ module Swift
161
+ def self.excluding(excluded)
162
+ Excluding.new(FileSelector.extensions(:swift), excluded)
163
+ end
164
+
165
+ def self.predefined(included:, excluded:)
166
+ Predefined.new(included:, extensions: FileSelector.extensions(:swift), excluded:)
167
+ end
168
+ end
145
169
  end
146
170
  end
@@ -29,6 +29,7 @@ module ChurnVsComplexity
29
29
  LanguageValidator.validate!(@language)
30
30
  @since_validator.validate!(since: @since, relative_period: nil)
31
31
  @complexity_validator.validate!(@language)
32
+ validate_max_gamma!
32
33
  end
33
34
 
34
35
  def checker
@@ -43,6 +44,12 @@ module ChurnVsComplexity
43
44
 
44
45
  private
45
46
 
47
+ def validate_max_gamma!
48
+ return unless @max_gamma.negative?
49
+
50
+ raise ValidationError, "Max gamma must not be negative, got #{@max_gamma}"
51
+ end
52
+
46
53
  def gate_serializer
47
54
  case @serializer
48
55
  when :json then Serializer::Json
@@ -24,10 +24,17 @@ module ChurnVsComplexity
24
24
  gamma = GammaScore.calculate(values[0], values[1]).round(2)
25
25
  next unless gamma > max_gamma
26
26
 
27
- exceeds_pct = (((gamma - max_gamma) / max_gamma) * 100).round(0)
28
- { file:, gamma_score: gamma, exceeds_by: "#{exceeds_pct}%" }
27
+ { file:, gamma_score: gamma, exceeds_by: excess(gamma, max_gamma) }
29
28
  end
30
29
  end
30
+
31
+ # There is no percentage by which a score exceeds zero, so a zero
32
+ # threshold reports the violation without one.
33
+ def excess(gamma, max_gamma)
34
+ return nil if max_gamma.zero?
35
+
36
+ "#{(((gamma - max_gamma) / max_gamma) * 100).round(0)}%"
37
+ end
31
38
  end
32
39
  end
33
40
  end
@@ -23,7 +23,7 @@ module ChurnVsComplexity
23
23
  def surrounding(commit:)
24
24
  current = object(commit)
25
25
  child_sha = `git -C #{@folder} log --reverse --ancestry-path #{current.sha}..HEAD --format=%H`.lines.first&.chomp
26
- next_commit = child_sha&.empty? ? nil : child_sha
26
+ next_commit = child_sha.nil? || child_sha.empty? ? nil : child_sha
27
27
  [current.parent&.sha, next_commit]
28
28
  end
29
29
 
@@ -39,8 +39,22 @@ module ChurnVsComplexity
39
39
  `#{command}`
40
40
  end
41
41
 
42
+ # Both ends are given an explicit time. A bare date is an approximate date
43
+ # to git, resolved against the clock and the timezone, so the window it
44
+ # produces is not the window that was asked for.
45
+ def commits_in(git_period)
46
+ @repo.log(1_000_000)
47
+ .since(start_of_day(git_period.effective_start_date))
48
+ .until(end_of_day(git_period.end_date))
49
+ .execute.to_a
50
+ end
51
+
52
+ def start_of_day(date) = date.strftime('%Y-%m-%dT00:00:00')
53
+
54
+ def end_of_day(date) = date.strftime('%Y-%m-%dT23:59:59')
55
+
42
56
  def resolve_commits_with_interval(git_period:, jump_days:)
43
- candidates = @repo.log(1_000_000).since(git_period.effective_start_date).until(git_period.end_date).to_a
57
+ candidates = commits_in(git_period)
44
58
 
45
59
  commits_by_date = candidates.filter { |c| c.date.to_date >= git_period.effective_start_date }
46
60
  .group_by { |c| c.date.to_date }
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ChurnVsComplexity
4
4
  module LanguageValidator
5
- SUPPORTED = %i[java ruby javascript python go kotlin].freeze
5
+ SUPPORTED = %i[java ruby javascript python go kotlin rust swift].freeze
6
6
 
7
7
  def self.validate!(language)
8
8
  raise ValidationError, "Unsupported language: #{language}" unless SUPPORTED.include?(language)
@@ -83,6 +83,22 @@ module ChurnVsComplexity
83
83
  serializer:,
84
84
  since: @since || @relative_period,
85
85
  )
86
+ when :rust
87
+ Engine.concurrent(
88
+ complexity: Complexity::RustCalculator,
89
+ churn:,
90
+ file_selector: FileSelector::Rust.excluding(@excluded),
91
+ serializer:,
92
+ since: @since || @relative_period,
93
+ )
94
+ when :swift
95
+ Engine.concurrent(
96
+ complexity: Complexity::SwiftCalculator,
97
+ churn:,
98
+ file_selector: FileSelector::Swift.excluding(@excluded),
99
+ serializer:,
100
+ since: @since || @relative_period,
101
+ )
86
102
  end
87
103
  end
88
104
 
@@ -11,18 +11,31 @@ module ChurnVsComplexity
11
11
 
12
12
  return empty_summary(end_date) if values_by_file.empty?
13
13
 
14
- churn_values = values_by_file.map { |_, values| values[0].to_f }
15
- complexity_values = values_by_file.map { |_, values| values[1].to_f }
16
- gamma_scores = values_by_file.map { |_, values| GammaScore.calculate(values[0], values[1]) }
17
-
18
- stats(churn_values).transform_keys { |k| :"#{k}_churn" }
19
- .merge(stats(complexity_values).transform_keys { |k| :"#{k}_complexity" })
20
- .merge(stats(gamma_scores).slice(:mean, :median).transform_keys { |k| :"#{k}_gamma_score" })
21
- .merge(end_date:)
14
+ summarise(values_by_file).merge(end_date:)
22
15
  end
23
16
 
24
17
  private
25
18
 
19
+ def summarise(values_by_file)
20
+ churn_stats(values_by_file)
21
+ .merge(complexity_stats(values_by_file), gamma_stats(values_by_file))
22
+ end
23
+
24
+ def churn_stats(values_by_file)
25
+ values = values_by_file.map { |_, pair| pair[0].to_f }
26
+ stats(values).transform_keys { |key| :"#{key}_churn" }
27
+ end
28
+
29
+ def complexity_stats(values_by_file)
30
+ values = values_by_file.map { |_, pair| pair[1].to_f }
31
+ stats(values).transform_keys { |key| :"#{key}_complexity" }
32
+ end
33
+
34
+ def gamma_stats(values_by_file)
35
+ values = values_by_file.map { |_, pair| GammaScore.calculate(pair[0], pair[1]) }
36
+ stats(values).slice(:mean, :median).transform_keys { |key| :"#{key}_gamma_score" }
37
+ end
38
+
26
39
  def stats(values)
27
40
  { mean: values.sum / values.size, median: values.sort[values.size / 2], max: values.max, min: values.min }
28
41
  end
@@ -15,6 +15,8 @@ module ChurnVsComplexity
15
15
  # Java PMD Cyclomatic complexity 1 ~39 1-40
16
16
  # Go gocognit Cognitive complexity 0 ~87 0-50
17
17
  # Kotlin lizard Cyclomatic complexity 1 ~40 1-40
18
+ # Rust lizard Cyclomatic complexity 1 ~40 1-40
19
+ # Swift lizard Cyclomatic complexity 1 ~40 1-40
18
20
  #
19
21
  # The DEFAULT_LOW and DEFAULT_HIGH thresholds below are rough midpoints
20
22
  # suitable for Java/Python/JS. They are too aggressive for Ruby (Flog
@@ -35,6 +35,8 @@ module ChurnVsComplexity
35
35
  end
36
36
 
37
37
  def make_chunks(commits)
38
+ return [] if commits.empty?
39
+
38
40
  chunk_size = (commits.size / 3.0).ceil
39
41
  commits.each_slice(chunk_size).map do |chunk|
40
42
  { chunk:, pipe: @factory.pipe }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChurnVsComplexity
4
- VERSION = '1.7.0'
4
+ VERSION = '1.9.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: churn_vs_complexity
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik T. Madsen
@@ -15,33 +15,33 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '4.8'
18
+ version: '4.9'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '4.8'
25
+ version: '4.9'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: git
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '2.1'
32
+ version: '5.0'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '2.1'
39
+ version: '5.0'
40
40
  description: Correlates file churn (how often files change) with complexity scores
41
41
  to identify refactoring hotspots. Supports Ruby, JavaScript/TypeScript, Java, Python,
42
- Go, and Kotlin. Modes include hotspots ranking, triage assessment, CI quality gate,
43
- diff comparison, focus sessions, and timetravel history. Inspired by Michael Feathers'
44
- article "Getting Empirical about Refactoring".
42
+ Go, Kotlin, Rust, and Swift. Modes include hotspots ranking, triage assessment,
43
+ CI quality gate, diff comparison, focus sessions, and timetravel history. Inspired
44
+ by Michael Feathers' article "Getting Empirical about Refactoring".
45
45
  email:
46
46
  - beatmadsen@gmail.com
47
47
  executables:
@@ -49,16 +49,9 @@ executables:
49
49
  extensions: []
50
50
  extra_rdoc_files: []
51
51
  files:
52
- - ".devcontainer/devcontainer.json"
53
- - ".rubocop.yml"
54
52
  - CHANGELOG.md
55
- - CLAUDE.md
56
- - Dockerfile
57
- - Dockerfile.dev
58
53
  - LICENSE.txt
59
54
  - README.md
60
- - Rakefile
61
- - TODO
62
55
  - bin/churn_vs_complexity
63
56
  - lib/churn_vs_complexity.rb
64
57
  - lib/churn_vs_complexity/churn.rb
@@ -74,6 +67,8 @@ files:
74
67
  - lib/churn_vs_complexity/complexity/pmd/files_calculator.rb
75
68
  - lib/churn_vs_complexity/complexity/pmd/folder_calculator.rb
76
69
  - lib/churn_vs_complexity/complexity/python_calculator.rb
70
+ - lib/churn_vs_complexity/complexity/rust_calculator.rb
71
+ - lib/churn_vs_complexity/complexity/swift_calculator.rb
77
72
  - lib/churn_vs_complexity/complexity_validator.rb
78
73
  - lib/churn_vs_complexity/concurrent_calculator.rb
79
74
  - lib/churn_vs_complexity/delta.rb
@@ -130,37 +125,11 @@ files:
130
125
  - lib/churn_vs_complexity/triage/config.rb
131
126
  - lib/churn_vs_complexity/triage/serializer.rb
132
127
  - lib/churn_vs_complexity/version.rb
133
- - package-lock.json
134
128
  - tmp/eslint-support/complexity-calculator.js
135
129
  - tmp/eslint-support/package.json
136
130
  - tmp/pmd-support/ruleset.xml
137
131
  - tmp/template/graph.html
138
132
  - tmp/template/timetravel_graph.html
139
- - tmp/test-support/delta/ruby-summary.txt
140
- - tmp/test-support/delta/ruby.csv
141
- - tmp/test-support/go/main.go
142
- - tmp/test-support/go/utils.go
143
- - tmp/test-support/java/small-example/src/main/java/org/example/Main.java
144
- - tmp/test-support/java/small-example/src/main/java/org/example/spice/Checker.java
145
- - tmp/test-support/javascript/complex.js
146
- - tmp/test-support/javascript/moderate.js
147
- - tmp/test-support/javascript/simple.js
148
- - tmp/test-support/javascript/typescript-example.ts
149
- - tmp/test-support/kotlin/Main.kt
150
- - tmp/test-support/kotlin/Utils.kt
151
- - tmp/test-support/python/example.py
152
- - tmp/test-support/python/utils.py
153
- - tmp/test-support/txt/abc.txt
154
- - tmp/test-support/txt/d.txt
155
- - tmp/test-support/txt/ef.txt
156
- - tmp/test-support/txt/ghij.txt
157
- - tmp/test-support/txt/klm.txt
158
- - tmp/test-support/txt/nopq.txt
159
- - tmp/test-support/txt/r.txt
160
- - tmp/test-support/txt/st.txt
161
- - tmp/test-support/txt/uvx.txt
162
- - tmp/test-support/txt/yz.txt
163
- - tmp/timetravel/.keep
164
133
  homepage: https://github.com/beatmadsen/churn_vs_complexity
165
134
  licenses:
166
135
  - MIT
@@ -182,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
182
151
  - !ruby/object:Gem::Version
183
152
  version: '0'
184
153
  requirements: []
185
- rubygems_version: 4.0.6
154
+ rubygems_version: 4.0.9
186
155
  specification_version: 4
187
156
  summary: Analyse churn vs complexity to find refactoring hotspots, gate CI quality,
188
157
  triage risky files, and track codebase health over time.
@@ -1,26 +0,0 @@
1
- // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2
- // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
3
- {
4
- "name": "Existing Dockerfile",
5
- "build": {
6
- // Sets the run context to one level up instead of the .devcontainer folder.
7
- "context": "..",
8
- // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
9
- "dockerfile": "../Dockerfile.dev"
10
- }
11
-
12
- // Features to add to the dev container. More info: https://containers.dev/features.
13
- // "features": {},
14
-
15
- // Use 'forwardPorts' to make a list of ports inside the container available locally.
16
- // "forwardPorts": [],
17
-
18
- // Uncomment the next line to run commands after the container is created.
19
- // "postCreateCommand": "cat /etc/os-release",
20
-
21
- // Configure tool-specific properties.
22
- // "customizations": {},
23
-
24
- // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
25
- // "remoteUser": "devcontainer"
26
- }
data/.rubocop.yml DELETED
@@ -1,25 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 3.3
3
- NewCops: enable
4
-
5
- Layout/LineLength:
6
- Max: 120
7
-
8
-
9
- Style/Documentation:
10
- Enabled: false
11
-
12
- Style/ModuleFunction:
13
- Enabled: false
14
-
15
- Style/TrailingCommaInArrayLiteral:
16
- Enabled: true
17
- EnforcedStyleForMultiline: consistent_comma
18
-
19
- Style/TrailingCommaInHashLiteral:
20
- Enabled: true
21
- EnforcedStyleForMultiline: consistent_comma
22
-
23
- Style/TrailingCommaInArguments:
24
- Enabled: true
25
- EnforcedStyleForMultiline: consistent_comma
data/CLAUDE.md DELETED
@@ -1,60 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- `churn_vs_complexity` is a Ruby gem that analyzes code quality by correlating file churn (how often files change) with complexity scores. It supports Ruby (via Flog), JavaScript/TypeScript (via ESLint), Java (via PMD), Python (via Radon), Go (via gocognit), and Kotlin (via lizard). Requires Ruby >= 3.3.
8
-
9
- ## Commands
10
-
11
- ```bash
12
- # Run tests (TLDR framework)
13
- bundle exec rake # default task runs tldr
14
- bundle exec tldr # direct
15
-
16
- # Run a single test file
17
- bundle exec tldr test/churn_vs_complexity/engine_test.rb
18
-
19
- # Run a single test by name
20
- bundle exec tldr --name test_something test/path/to_test.rb
21
-
22
- # Lint
23
- bundle exec rubocop
24
- bundle exec rubocop -a # auto-fix safe offenses
25
-
26
- # Build/install gem
27
- bundle exec rake build
28
- bundle exec rake install
29
- ```
30
-
31
- ## Architecture
32
-
33
- Three operating modes, each with its own Config/Checker/Serializer pipeline:
34
-
35
- - **Normal** (`Normal::Config` → `Engine` → `ConcurrentCalculator`): Analyzes a folder, computing churn + complexity per file. Outputs CSV, HTML graph, or text summary.
36
- - **Timetravel** (`Timetravel::Config` → `Traveller`): Samples quality scores across historical commits at N-day intervals. Uses forked processes with git worktrees for isolation.
37
- - **Delta** (`Delta::Config` → `MultiChecker` → `Checker`): Analyzes complexity of files changed in specific commits. Uses threads with git worktrees.
38
-
39
- Entry point: `CLI::run!` → `CLI::Parser` → `CLI::Main.run!` → mode-specific Config → checker.
40
-
41
- ### Key interfaces
42
-
43
- - **Complexity calculators**: `folder_based?`, `calculate(folder:)` or `calculate(files:)`
44
- - **Churn calculators**: `calculate(folder:, file:, since:)`, `date_of_latest_commit(folder:)`
45
- - **Serializers**: `serialize(result)`
46
- - **File selectors**: `select_files(folder)` → `{ included: [...], explicitly_excluded: [...] }`
47
-
48
- ### Concurrency model
49
-
50
- - Normal: threads (nprocessors) for per-file churn calculation
51
- - Timetravel: forked child processes with IO.pipe for IPC
52
- - Delta: threads (2x nprocessors) for concurrent commit analysis
53
-
54
- ## Code Style
55
-
56
- RuboCop config (`.rubocop.yml`):
57
- - Max line length: 120
58
- - Trailing commas enforced on multiline (arrays, hashes, arguments)
59
- - `NewCops: enable`
60
- - No class/module documentation required (`Style::Documentation` disabled)
data/Dockerfile DELETED
@@ -1,34 +0,0 @@
1
- # Create a build image with wget and unzip
2
- FROM ubuntu:latest as pmd_builder
3
- # Install wget and unzip
4
- RUN apt-get update && \
5
- apt-get install -y --no-install-recommends \
6
- wget unzip
7
-
8
- RUN wget https://github.com/pmd/pmd/releases/download/pmd_releases%2F7.0.0/pmd-dist-7.0.0-bin.zip --no-check-certificate
9
- RUN unzip pmd-dist-7.0.0-bin.zip
10
-
11
- FROM ruby:3.3-bullseye
12
- # Install git and java JDK
13
-
14
- RUN apt-get update; \
15
- apt-get install -y --no-install-recommends \
16
- git openjdk-17-jdk
17
-
18
- # copy PMD from build image
19
- COPY --from=pmd_builder /pmd-bin-7.0.0 /usr/local/bin/pmd-bin-7.0.0
20
- # make pmd available in the PATH
21
- RUN ln -s /usr/local/bin/pmd-bin-7.0.0/bin/pmd /usr/local/bin/pmd
22
-
23
- COPY *.gemspec Gemfile* /app/
24
- COPY lib /app/lib
25
-
26
- WORKDIR /app
27
-
28
- RUN bundle install
29
-
30
- COPY bin /app/bin
31
- COPY tmp/pmd-support /app/tmp/pmd-support
32
- COPY tmp/template /app/tmp/template
33
-
34
- CMD ["/bin/sh", "-c", "echo 'It works!'"]
data/Dockerfile.dev DELETED
@@ -1,27 +0,0 @@
1
- # Create a build image with wget and unzip
2
- FROM ubuntu:latest as pmd_builder
3
- # Install wget and unzip
4
- RUN apt-get update && \
5
- apt-get install -y --no-install-recommends \
6
- wget unzip
7
-
8
- RUN wget https://github.com/pmd/pmd/releases/download/pmd_releases%2F7.0.0/pmd-dist-7.0.0-bin.zip --no-check-certificate
9
- RUN unzip pmd-dist-7.0.0-bin.zip
10
-
11
- FROM ruby:3.3-bullseye
12
- # Install git and java JDK
13
-
14
- RUN apt-get update; \
15
- apt-get install -y --no-install-recommends \
16
- git openjdk-17-jdk
17
-
18
- # copy PMD from build image
19
- COPY --from=pmd_builder /pmd-bin-7.0.0 /usr/local/bin/pmd-bin-7.0.0
20
- # make pmd available in the PATH
21
- RUN ln -s /usr/local/bin/pmd-bin-7.0.0/bin/pmd /usr/local/bin/pmd
22
-
23
- COPY *.gemspec Gemfile* ./
24
- COPY lib/churn_vs_complexity/version.rb lib/churn_vs_complexity/version.rb
25
- RUN bundle install
26
-
27
- CMD ["/bin/sh", "-c", "echo 'It works!'"]
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/gem_tasks'
4
- require 'tldr/rake'
5
-
6
- task default: :tldr
data/TODO DELETED
@@ -1,11 +0,0 @@
1
- TODO:
2
-
3
- - Move Timetravel calculations away from serializer
4
-
5
- - Database, where we can prepopulate the state of every file and every commit
6
- - Populate incrementally from each commit in log.
7
- - Only need to care about deltas between commits, and copy everything else from previous commit.
8
- - processed_commit table with sha and version of processing logic
9
- - Unit tests for simpler new classes
10
- - Integration test for Timetravel
11
- - Candlebars on mean dots in graph
data/package-lock.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "name": "churn_vs_complexity",
3
- "lockfileVersion": 3,
4
- "requires": true,
5
- "packages": {}
6
- }
@@ -1,55 +0,0 @@
1
- Commit: b20a9bddd04afb9c7a736cf4530fd3188b5f785e
2
- Parent: 4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0
3
- Next: 8cdaac516365bd7007b9f755bbe6f6e86d8e13dd
4
-
5
-
6
- File, relative path: lib/churn_vs_complexity/serializer/timetravel.rb
7
- Type of change: modified
8
- Complexity: 58.34326171010786
9
-
10
- File, relative path: lib/churn_vs_complexity/serializer/timetravel/stats_calculator.rb
11
- Type of change: new
12
- Complexity: 80.35549194682164
13
-
14
- File, relative path: test/churn_vs_complexity/serializer/timetravel/quality_calculator_test.rb
15
- Type of change: modified
16
- Complexity: 13.054183368177016
17
-
18
- File, relative path: test/churn_vs_complexity/serializer/timetravel/stats_calculator_test.rb
19
- Type of change: new
20
- Complexity: 12.2198021565134
21
-
22
-
23
-
24
- Commit: 4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0
25
- Parent: 83deeccf8aa40e46cd0ca7e2c603c9726633256e
26
- Next: b20a9bddd04afb9c7a736cf4530fd3188b5f785e
27
-
28
-
29
- File, relative path: lib/churn_vs_complexity/config.rb
30
- Type of change: modified
31
- Complexity: 70.26655854867414
32
-
33
- File, relative path: lib/churn_vs_complexity/serializer/pass_through.rb
34
- Type of change: new
35
- Complexity: 19.309088021254183
36
-
37
- File, relative path: lib/churn_vs_complexity/serializer/summary_hash.rb
38
- Type of change: modified
39
- Complexity: 89.25682464940157
40
-
41
- File, relative path: lib/churn_vs_complexity/serializer/timetravel.rb
42
- Type of change: modified
43
- Complexity: 131.0989019732436
44
-
45
- File, relative path: lib/churn_vs_complexity/serializer/timetravel/quality_calculator.rb
46
- Type of change: new
47
- Complexity: 23.324498016752965
48
-
49
- File, relative path: test/churn_vs_complexity/serializer/summary_hash_test.rb
50
- Type of change: modified
51
- Complexity: 14.396336540125482
52
-
53
- File, relative path: test/churn_vs_complexity/serializer/timetravel/quality_calculator_test.rb
54
- Type of change: new
55
- Complexity: 13.054183368177016
@@ -1,12 +0,0 @@
1
- Commit, Relative Path, Type of Change, Complexity
2
- b20a9bddd04afb9c7a736cf4530fd3188b5f785e, lib/churn_vs_complexity/serializer/timetravel.rb, modified, 58.34326171010786
3
- b20a9bddd04afb9c7a736cf4530fd3188b5f785e, lib/churn_vs_complexity/serializer/timetravel/stats_calculator.rb, new, 80.35549194682164
4
- b20a9bddd04afb9c7a736cf4530fd3188b5f785e, test/churn_vs_complexity/serializer/timetravel/quality_calculator_test.rb, modified, 13.054183368177016
5
- b20a9bddd04afb9c7a736cf4530fd3188b5f785e, test/churn_vs_complexity/serializer/timetravel/stats_calculator_test.rb, new, 12.2198021565134
6
- 4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, lib/churn_vs_complexity/config.rb, modified, 70.26655854867414
7
- 4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, lib/churn_vs_complexity/serializer/pass_through.rb, new, 19.309088021254183
8
- 4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, lib/churn_vs_complexity/serializer/summary_hash.rb, modified, 89.25682464940157
9
- 4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, lib/churn_vs_complexity/serializer/timetravel.rb, modified, 131.0989019732436
10
- 4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, lib/churn_vs_complexity/serializer/timetravel/quality_calculator.rb, new, 23.324498016752965
11
- 4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, test/churn_vs_complexity/serializer/summary_hash_test.rb, modified, 14.396336540125482
12
- 4f3151f83e982eb8f2b8a4e7a7572a0af156c3c0, test/churn_vs_complexity/serializer/timetravel/quality_calculator_test.rb, new, 13.054183368177016
@@ -1,11 +0,0 @@
1
- package main
2
-
3
- import "fmt"
4
-
5
- func main() {
6
- fmt.Println("Hello, world!")
7
- }
8
-
9
- func add(a, b int) int {
10
- return a + b
11
- }
@@ -1,13 +0,0 @@
1
- package main
2
-
3
- func calculateSum(numbers []int) int {
4
- total := 0
5
- for _, n := range numbers {
6
- total += n
7
- }
8
- return total
9
- }
10
-
11
- func isEven(n int) bool {
12
- return n%2 == 0
13
- }
@@ -1,10 +0,0 @@
1
- package org.example;
2
-
3
- import org.example.spice.Checker;
4
-
5
- public class Main {
6
- public static void main(String[] args) {
7
- new Checker().check();
8
- }
9
-
10
- }
@@ -1,28 +0,0 @@
1
- package org.example.spice;
2
-
3
- import java.util.Iterator;
4
- import java.util.PrimitiveIterator;
5
- import java.util.Random;
6
-
7
- public class Checker {
8
- public void check() {
9
- for (int j = 0; j < 3; j++) {
10
- var iter = tenRandomInts();
11
- while (iter.hasNext()) {
12
- var i = iter.nextInt();
13
- if (i < 100) {
14
- System.out.println("Bongo");
15
- } else if (i > 200) {
16
- System.out.println("Dingo");
17
- } else {
18
- System.out.println("Zapp");
19
- }
20
- }
21
- }
22
- }
23
-
24
- private static PrimitiveIterator.OfInt tenRandomInts() {
25
- var randomInts = new Random().ints();
26
- return randomInts.limit(10).iterator();
27
- }
28
- }
@@ -1,43 +0,0 @@
1
- function analyzeNumber(num) {
2
- let result = '';
3
-
4
- if (num < 0) {
5
- result += 'negative ';
6
- } else if (num > 0) {
7
- result += 'positive ';
8
- } else {
9
- return 'zero';
10
- }
11
-
12
- if (num % 2 === 0) {
13
- result += 'even ';
14
- } else {
15
- result += 'odd ';
16
- }
17
-
18
- if (num % 3 === 0) {
19
- result += 'divisible by 3 ';
20
- }
21
-
22
- if (num % 5 === 0) {
23
- result += 'divisible by 5 ';
24
- }
25
-
26
- if (isPrime(num)) {
27
- result += 'prime ';
28
- }
29
-
30
- return result.trim();
31
- }
32
-
33
- function isPrime(num) {
34
- if (num <= 1) return false;
35
- for (let i = 2; i <= Math.sqrt(num); i++) {
36
- if (num % i === 0) return false;
37
- }
38
- return true;
39
- }
40
-
41
- console.log(analyzeNumber(17));
42
- console.log(analyzeNumber(30));
43
- console.log(analyzeNumber(-7));
@@ -1,12 +0,0 @@
1
- function fibonacci(n) {
2
- if (n <= 1) return n;
3
- return fibonacci(n - 1) + fibonacci(n - 2);
4
- }
5
-
6
- function printFibonacciSequence(length) {
7
- for (let i = 0; i < length; i++) {
8
- console.log(fibonacci(i));
9
- }
10
- }
11
-
12
- printFibonacciSequence(10);
@@ -1,5 +0,0 @@
1
- function greet(name) {
2
- return `Hello, ${name}!`;
3
- }
4
-
5
- console.log(greet('World'));
@@ -1,26 +0,0 @@
1
- interface Person {
2
- name: string;
3
- age: number;
4
- }
5
-
6
- function createGreeting(person: Person): string {
7
- let greeting = `Hello, ${person.name}!`;
8
-
9
- if (person.age < 18) {
10
- greeting += " You're still a minor.";
11
- } else if (person.age >= 18 && person.age < 65) {
12
- greeting += " You're an adult.";
13
- } else {
14
- greeting += " You're a senior citizen.";
15
- }
16
-
17
- return greeting;
18
- }
19
-
20
- const alice: Person = { name: 'Alice', age: 30 };
21
- const bob: Person = { name: 'Bob', age: 17 };
22
- const charlie: Person = { name: 'Charlie', age: 70 };
23
-
24
- console.log(createGreeting(alice));
25
- console.log(createGreeting(bob));
26
- console.log(createGreeting(charlie));
@@ -1,7 +0,0 @@
1
- fun main() {
2
- println("Hello, world!")
3
- }
4
-
5
- fun add(a: Int, b: Int): Int {
6
- return a + b
7
- }
@@ -1,11 +0,0 @@
1
- fun calculateSum(numbers: List<Int>): Int {
2
- var total = 0
3
- for (n in numbers) {
4
- total += n
5
- }
6
- return total
7
- }
8
-
9
- fun isEven(n: Int): Boolean {
10
- return n % 2 == 0
11
- }
@@ -1,6 +0,0 @@
1
- def hello():
2
- print("Hello, world!")
3
-
4
-
5
- def add(a, b):
6
- return a + b
@@ -1,9 +0,0 @@
1
- def calculate_sum(numbers):
2
- total = 0
3
- for n in numbers:
4
- total += n
5
- return total
6
-
7
-
8
- def is_even(n):
9
- return n % 2 == 0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
data/tmp/timetravel/.keep DELETED
File without changes